local builtin = require("telescope.builtin")
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local action_set = require("telescope.actions.set")
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local scan = require("plenary.scandir")
local utils = require("telescope.utils")
local previewers = require("telescope.previewers")
local make_entry = require("telescope.make_entry")
local entry_display = require("telescope.pickers.entry_display")
local sorters = require("telescope.sorters")
local themes = require("telescope.themes")
local debug_utils = require("plenary.debug_utils")
local filetype = require("plenary.filetype")
local taglinks = require("telekasten.utils.taglinks")
local tagutils = require("telekasten.utils.tags")
local linkutils = require("telekasten.utils.links")
local dateutils = require("telekasten.utils.dates")
local fileutils = require("telekasten.utils.files")
local templates = require("telekasten.templates")
local Path = require("plenary.path")
local tkpickers = require("telekasten.pickers")
local tkutils = require("telekasten.utils")

-- declare locals for the nvim api stuff to avoid more lsp warnings
local vim = vim

-- ----------------------------------------------------------------------------
-- DEFAULT CONFIG
-- ----------------------------------------------------------------------------
local _home = vim.fn.expand("~/zettelkasten")
local M = {}
local function defaultConfig(home)
    if home == nil then
        home = _home
    end

    local cfg = {
        home = home,
        -- if true, telekasten will be enabled when opening a note within the configured home
        take_over_my_home = true,
        -- auto-set telekasten filetype: if false, the telekasten filetype will not be used
        --                               and thus the telekasten syntax will not be loaded either
        auto_set_filetype = true,
        -- auto-set telekasten syntax: if false, the telekasten syntax will not be set
        -- this syntax setting is independent from auto-set filetype
        auto_set_syntax = true,
        -- dir names for special notes (absolute path or subdir name)
        dailies = home,
        weeklies = home,
        monthlies = home,
        quarterlies = home,
        yearlies = home,
        templates = home,
        -- image (sub)dir for pasting
        -- dir name (absolute path or subdir name)
        -- or nil if pasted images shouldn't go into a special subdir
        image_subdir = nil,
        -- markdown file extension
        extension = ".md",
        -- Generate note filenames. One of:
        -- "title" (default) - Use title if supplied, uuid otherwise
        -- "uuid" - Use uuid
        -- "uuid-title" - Prefix title by uuid
        -- "title-uuid" - Suffix title with uuid
        new_note_filename = "title",
        --[[ file UUID type
           - "rand"
           - string input for os.date()
           - or custom lua function that returns a string
        --]]
        uuid_type = "%Y%m%d%H%M",
        -- UUID separator
        uuid_sep = "-",
        -- if not nil, replaces any spaces in the title when it is used in filename generation
        filename_space_subst = nil,
        -- if true, make the filename lowercase
        filename_small_case = false,
        -- following a link to a non-existing note will create it
        follow_creates_nonexisting = true,
        dailies_create_nonexisting = true,
        weeklies_create_nonexisting = true,
        monthlies_create_nonexisting = true,
        quarterlies_create_nonexisting = true,
        yearlies_create_nonexisting = true,
        -- allow following links to files outside the current vault (absolute paths)
        external_link_follow = true,
        -- skip telescope prompt for goto_today and goto_thisweek
        journal_auto_open = false,
        -- templates for new notes
        -- template_new_note = home .. "/" .. "templates/new_note.md",
        -- template_new_daily = home .. "/" .. "templates/daily_tk.md",
        -- template_new_weekly = home .. "/" .. "templates/weekly_tk.md",
        -- template_new_monthly = home .. "/" .. "templates/monthly_tk.md",
        -- template_new_quarterly = home .. "/" .. "templates/quarterly_tk.md"
        -- template_new_yearly = home .. "/" .. "templates/yearly_tk.md"

        -- image link style
        -- wiki:     ![[image name]]
        -- markdown: ![](image_subdir/xxxxx.png)
        image_link_style = "markdown",
        -- default sort option: 'filename', 'modified'
        sort = "filename",
        -- when linking to a note in subdir/, create a [[subdir/title]] link
        -- instead of a [[title only]] link
        subdirs_in_links = true,
        -- integrate with calendar-vim
        plug_into_calendar = true,
        calendar_opts = {
            -- calendar week display mode: 1 .. 'WK01', 2 .. 'WK 1', 3 .. 'KW01', 4 .. 'KW 1', 5 .. '1'
            weeknm = 4,
            -- use monday as first day of week: 1 .. true, 0 .. false
            calendar_monday = 1,
            -- calendar mark: where to put mark for marked days: 'left', 'right', 'left-fit'
            calendar_mark = "left-fit",
        },
        close_after_yanking = false,
        insert_after_inserting = true,
        -- tag notation: '#tag', ':tag:', 'yaml-bare'
        tag_notation = "#tag",
        -- command palette theme: dropdown (window) or ivy (bottom panel)
        command_palette_theme = "ivy",
        -- tag list theme:
        -- get_cursor: small tag list at cursor; ivy and dropdown like above
        show_tags_theme = "ivy",
        -- template_handling
        -- What to do when creating a new note via `new_note()` or `follow_link()`
        -- to a non-existing note
        -- - prefer_new_note: use `new_note` template
        -- - smart: if day or week is detected in title, use daily / weekly templates (default)
        -- - always_ask: always ask before creating a note
        template_handling = "smart",
        -- path handling:
        --   this applies to:
        --     - new_note()
        --     - new_templated_note()
        --     - follow_link() to non-existing note
        --
        --   it does NOT apply to:
        --     - goto_today()
        --     - goto_thisweek()
        --
        --   Valid options:
        --     - smart: put daily-looking notes in daily, weekly-looking ones in weekly,
        --              all other ones in home, except for notes/with/subdirs/in/title.
        --              (default)
        --
        --     - prefer_home: put all notes in home except for goto_today(), goto_thisweek()
        --                    except for notes/with/subdirs/in/title.
        --
        --     - same_as_current: put all new notes in the dir of the current note if
        --                        present or else in home
        --                        except for notes/with/subdirs/in/title.
        new_note_location = "smart",
        -- should all links be updated when a file is renamed
        rename_update_links = true,
        -- how to preview media files
        -- "telescope-media-files" if you have telescope-media-files.nvim installed
        -- "catimg-previewer" if you have catimg installed
        -- "viu-previewer" if you have viu installed
        media_previewer = "telescope-media-files",
        -- files which will be available in insert and preview images list
        media_extensions = {
            ".png",
            ".jpg",
            ".bmp",
            ".gif",
            ".pdf",
            ".mp4",
            ".webm",
            ".webp",
        },
        -- A customizable fallback handler for urls.
        follow_url_fallback = nil,
        -- Enable creation new notes with Ctrl-n when finding notes
        enable_create_new = true,

        -- Specify a clipboard program to use
        clipboard_program = "", -- xsel, xclip, wl-paste, osascript
    }
    M.Cfg = cfg
    M.note_type_templates = {
        normal = M.Cfg.template_new_note,
        daily = M.Cfg.template_new_daily,
        weekly = M.Cfg.template_new_weekly,
        monthly = M.Cfg.template_new_monthly,
        quarterly = M.Cfg.template_new_quarterly,
        yearly = M.Cfg.template_new_yearly,
    }
end

local function generate_note_filename(uuid, title)
    if M.Cfg.filename_space_subst ~= nil then
        title = title:gsub(" ", M.Cfg.filename_space_subst)
    end

    if M.Cfg.filename_small_case then
        title = string.lower(title)
    end

    local pp = Path:new(title)
    local p_splits = pp:_split()
    local filename = p_splits[#p_splits]
    local subdir = title:gsub(tkutils.escape(filename), "")

    local sep = M.Cfg.uuid_sep or "-"
    if M.Cfg.new_note_filename ~= "uuid" and #title > 0 then
        if M.Cfg.new_note_filename == "uuid-title" then
            return subdir .. uuid .. sep .. filename
        elseif M.Cfg.new_note_filename == "title-uuid" then
            return title .. sep .. uuid
        else
            return title
        end
    else
        return uuid
    end
end

local function check_dir_and_ask(dir, purpose, callback)
    local ret = false
    if dir ~= nil and Path:new(dir):exists() == false then
        vim.ui.select({ "No (default)", "Yes" }, {
            prompt = "Telekasten.nvim: "
                .. purpose
                .. " folder "
                .. dir
                .. " does not exist!"
                .. " Shall I create it? ",
        }, function(answer)
            if answer == "Yes" then
                if
                    Path:new(dir):mkdir({ parents = true, exists_ok = false })
                then
                    vim.cmd('echomsg " "')
                    vim.cmd('echomsg "' .. dir .. ' created"')
                    ret = true
                    callback(ret)
                else
                    -- unreachable: plenary.Path:mkdir() will error out
                    tkutils.print_error("Could not create directory " .. dir)
                    ret = false
                    callback(ret)
                end
            end
        end)
    else
        ret = true
        if callback ~= nil then
            callback(ret)
        end
        return ret
    end
end

local function global_dir_check(callback)
    local ret
    if M.Cfg.home == nil then
        tkutils.print_error("Telekasten.nvim: home is not configured!")
        ret = false
        callback(ret)
    end
    local check = check_dir_and_ask
    -- nested callbacks to handle asynchronous vim.ui.select
    -- looks a little confusing but execution is sequential from top to bottom
    check(M.Cfg.home, "home", function()
        check(M.Cfg.dailies, "dailies", function()
            check(M.Cfg.weeklies, "weeklies", function()
                check(M.Cfg.monthlies, "monthlies", function()
                    check(M.Cfg.quarterlies, "quarterlies", function()
                        check(M.Cfg.yearlies, "yearlies", function()
                            check(M.Cfg.templates, "templates", function()
                                -- Note the `callback` in this last function call
                                check(M.Cfg.image_subdir, "images", callback)
                            end)
                        end)
                    end)
                end)
            end)
        end)
    end)
end

local function make_config_path_absolute(path)
    local ret = path
    if not (Path:new(path):is_absolute()) and path ~= nil then
        ret = M.Cfg.home .. "/" .. path
    end

    if ret ~= nil then
        ret = ret:gsub("/$", "")
    end

    return ret
end

local function recursive_substitution(dir, old, new)
    print("Searching for links to '" .. old .. "' in directory: " .. dir)

    global_dir_check(function(dir_check)
        if not dir_check then
            print("Directory check failed for: " .. dir)
            return
        end

        -- Use Lua-based file processing instead of external tools
        local files = scan.scan_dir(dir, {
            hidden = false,
            add_dirs = false,
            depth = 100, -- reasonable depth limit
        })

        print("Found " .. #files .. " files to check in " .. dir)

        for _, file in ipairs(files) do
            -- Only process markdown files
            if file:match("%.md$") then
                local success, content = pcall(vim.fn.readfile, file)
                if success and content then
                    local modified = false
                    local new_content = {}

                    for _, line in ipairs(content) do
                        local new_line = line
                        -- Handle different link formats:
                        -- [[oldtitle]] -> [[newtitle]]
                        -- [[oldtitle#heading]] -> [[newtitle#heading]]
                        -- [[oldtitle#^paragraph]] -> [[newtitle#^paragraph]]
                        -- [[oldtitle|alias]] -> [[newtitle|alias]]
                        -- [[oldtitle#heading|alias]] -> [[newtitle#heading|alias]]

                        -- Pattern to match [[oldtitle]] and variations
                        local pattern = "%[%["
                            .. vim.pesc(old)
                            .. "([^%]]*)%]%]"
                        local replacement = "[[" .. new .. "%1]]"

                        local new_line_updated =
                            new_line:gsub(pattern, replacement)
                        if new_line_updated ~= new_line then
                            modified = true
                            print(
                                "Found link to update: "
                                    .. line
                                    .. " -> "
                                    .. new_line_updated
                            )
                        end
                        table.insert(new_content, new_line_updated)
                    end

                    -- Write back the modified content if changes were made
                    if modified then
                        local write_success =
                            pcall(vim.fn.writefile, new_content, file)
                        if write_success then
                            print("Updated links in: " .. file)
                        else
                            print("Error writing to: " .. file)
                        end
                    end
                else
                    print("Error reading file: " .. file)
                end
            end
        end
    end)
end

local function save_all_mod_buffers()
    for i = 1, vim.fn.bufnr("$") do
        if
            vim.fn.getbufvar(i, "&mod") == 1
            and (
                (
                    M.Cfg.auto_set_filetype == true
                    and vim.fn.getbufvar(i, "&filetype") == "telekasten"
                ) or M.Cfg.auto_set_filetype == false
            )
        then
            vim.cmd(i .. "bufdo w")
        end
    end
end

-- ----------------------------------------------------------------------------
-- image stuff
-- ----------------------------------------------------------------------------

local function make_relative_path(bufferpath, imagepath, sep)
    sep = sep or "/"

    -- Split the buffer and image path into their dirs/files
    local buffer_dirs = {}
    for w in string.gmatch(bufferpath, "([^" .. sep .. "]+)") do
        buffer_dirs[#buffer_dirs + 1] = w
    end
    local image_dirs = {}
    for w in string.gmatch(imagepath, "([^" .. sep .. "]+)") do
        image_dirs[#image_dirs + 1] = w
    end

    -- The parts of the dir list that match won't matter, so skip them
    local i = 1
    while i < #image_dirs and i < #buffer_dirs do
        if image_dirs[i] ~= buffer_dirs[i] then
            break
        else
            i = i + 1
        end
    end

    -- Append ../ to walk up from the buffer location and the path downward
    -- to the location of the image file in order to create a relative path
    local relative_path = ""
    while i <= #image_dirs or i <= #buffer_dirs do
        if i <= #image_dirs then
            if relative_path == "" then
                relative_path = image_dirs[i]
            else
                relative_path = relative_path .. sep .. image_dirs[i]
            end
        end
        if i <= #buffer_dirs - 1 then
            relative_path = ".." .. sep .. relative_path
        end
        i = i + 1
    end

    return relative_path
end

local function imgFromClipboard()
    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local paste_command = {}
        paste_command["xsel"] = function(dir, filename)
            local _image_path = vim.fn.system("xsel --clipboard --output")
            local image_path = _image_path:gsub("file://", "")
            if
                vim.fn
                    .system("file --mime-type -b " .. image_path)
                    :gsub("%s+", "")
                == "image/png"
            then
                return "cp " .. image_path .. " " .. dir .. "/" .. filename
            else
                return ""
            end
        end
        paste_command["xclip"] = function(dir, filename)
            return "xclip -selection clipboard -t image/png -o > "
                .. dir
                .. "/"
                .. filename
        end
        paste_command["osascript"] = function(dir, filename)
            return string.format(
                'osascript -e "tell application \\"System Events\\" to write (the clipboard as «class PNGf») to '
                    .. '(make new file at folder \\"%s\\" with properties {name:\\"%s\\"})"',
                dir,
                filename
            )
        end

        paste_command["wl-paste"] = function(dir, filename)
            return "wl-paste -n -t image/png > " .. dir .. "/" .. filename
        end

        local get_paste_command
        if paste_command[M.Cfg.clipboard_program] ~= nil then
            if vim.fn.executable(M.Cfg.clipboard_program) ~= 1 then
                vim.api.nvim_err_write(
                    "The clipboard program specified [`"
                        .. M.cfg.clipboard_program
                        .. "`] is not executable or not in your $PATH\n"
                )
            end
            get_paste_command = paste_command[M.Cfg.clipboard_program]
        elseif vim.fn.executable("xsel") == 1 then
            get_paste_command = paste_command["xsel"]
        elseif vim.fn.executable("xclip") == 1 then
            get_paste_command = paste_command["xclip"]
        elseif vim.fn.executable("wl-paste") == 1 then
            get_paste_command = paste_command["wl-paste"]
        elseif vim.fn.executable("osascript") == 1 then
            get_paste_command = paste_command["osascript"]
        else
            vim.api.nvim_err_write(
                "No clipboard programs found!\nChecked executables: xsel, xclip, wl-paste, osascript\n"
            )
        end

        -- TODO: check `xclip -selection clipboard -t TARGETS -o` for the occurrence of `image/png`

        -- using plenary.job::new():sync() with on_stdout(_, data) unfortunately did some weird ASCII translation on the
        -- data, so the PNGs were invalid. It seems like 0d 0a and single 0a bytes were stripped by the plenary job:
        --
        -- plenary job version:
        -- $ hexdump -C /tmp/x.png|head
        -- 00000000  89 50 4e 47 1a 00 00 00  49 48 44 52 00 00 03 19  |.PNG....IHDR....|
        -- 00000010  00 00 01 c1 08 02 00 00  00 8a 73 e1 c3 00 00 00  |..........s.....|
        -- 00000020  09 70 48 59 73 00 00 0e  c4 00 00 0e c4 01 95 2b  |.pHYs..........+|
        -- 00000030  0e 1b 00 00 20 00 49 44  41 54 78 9c ec dd 77 58  |.... .IDATx...wX|
        -- 00000040  14 d7 fa 07 f0 33 bb b3  4b af 0b 2c 08 22 1d 04  |.....3..K..,."..|
        -- 00000050  05 11 10 1b a2 54 c5 1e  bb b1 c6 98 c4 68 72 4d  |.....T.......hrM|
        -- 00000060  e2 cd 35 37 26 b9 49 6e  6e 7e f7 a6 98 98 a8 29  |..57&.Inn~.....)|
        -- 00000070  26 6a 8c 51 63 8b bd 00  8a 58 40 b0 81 08 2a 45  |&j.Qc....X@...*E|
        -- 00000080  69 52 17 58 ca ee b2 f5  f7 c7 ea 4a 10 66 d7 01  |iR.X.......J.f..|
        -- 00000090  b1 e4 fb 79 7c f2 2c e7  cc 39 e7 3d 67 66 b3 2f  |...y|.,..9.=gf./|
        --
        -- OK version
        -- $ hexdump -C /tmp/x2.png|head
        -- 00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
        -- 00000010  00 00 03 19 00 00 01 c1  08 02 00 00 00 8a 73 e1  |..............s.|
        -- 00000020  c3 00 00 00 09 70 48 59  73 00 00 0e c4 00 00 0e  |.....pHYs.......|
        -- 00000030  c4 01 95 2b 0e 1b 00 00  20 00 49 44 41 54 78 9c  |...+.... .IDATx.|
        -- 00000040  ec dd 77 58 14 d7 fa 07  f0 33 bb b3 4b af 0b 2c  |..wX.....3..K..,|
        -- 00000050  08 22 1d 04 05 11 10 1b  a2 54 c5 1e bb b1 c6 98  |.".......T......|
        -- 00000060  c4 68 72 4d e2 cd 35 37  26 b9 49 6e 6e 7e f7 a6  |.hrM..57&.Inn~..|
        -- 00000070  98 98 a8 29 26 6a 8c 51  63 8b bd 00 8a 58 40 b0  |...)&j.Qc....X@.|
        -- 00000080  81 08 2a 45 69 52 17 58  ca ee b2 f5 f7 c7 ea 4a  |..*EiR.X.......J|
        -- 00000090  10 66 d7 01 b1 e4 fb 79  7c f2 2c e7 cc 39 e7 3d  |.f.....y|.,..9.=|

        local pngname = "pasted_img_" .. os.date("%Y%m%d%H%M%S") .. ".png"
        local pngdir = M.Cfg.image_subdir and M.Cfg.image_subdir or M.Cfg.home
        local png = Path:new(pngdir, pngname).filename
        local relpath = make_relative_path(vim.fn.expand("%:p"), png, "/")

        local output = vim.fn.system(get_paste_command(pngdir, pngname))
        if output ~= "" then
            -- Remove empty file created by previous command if failed
            vim.fn.system("rm " .. png)
            vim.api.nvim_err_writeln(
                string.format(
                    "Unable to write image %s.\n"
                        .. "Is there an image on the clipboard?\n"
                        .. "Have you set clipboard_program to your preferred paste command? "
                        .. "(see :help telekasten.configuration)\n"
                        .. "See also issue 131",
                    png
                )
            )
        end

        if fileutils.file_exists(png) then
            if M.Cfg.image_link_style == "markdown" then
                vim.api.nvim_put({ "![](" .. relpath .. ")" }, "", true, true)
            else
                vim.api.nvim_put({ "![[" .. pngname .. "]]" }, "", true, true)
            end
        else
            vim.api.nvim_err_writeln("Unable to write image " .. png)
        end
    end)
end

-- end of image stuff

local function create_note_from_template(
    title,
    uuid,
    filepath,
    templatefn,
    calendar_info,
    callback
)
    -- first, read the template file
    local lines = {}
    if fileutils.file_exists(templatefn) then
        for line in io.lines(templatefn) do
            lines[#lines + 1] = line
        end
    end

    -- now write the output file, substituting vars line by line
    local file_dir = filepath:match("(.*/)") or ""
    check_dir_and_ask(file_dir, "Notes", function(dir_succeed)
        if dir_succeed == false then
            return
        end

        local ofile = io.open(filepath, "a")

        for _, line in pairs(lines) do
            ofile:write(
                templates.subst_templated_values(
                    line,
                    title,
                    calendar_info,
                    uuid,
                    M.Cfg.calendar_opts.calendar_monday
                ) .. "\n"
            )
        end

        ofile:flush()
        ofile:close()
        callback()
    end)
end

--- Pinfo
---    - fexists : true if file exists
---    - title : the title (filename including subdirs without extension)
---      - if opts.subdirs_in_links is false, no subdirs will be included
---    - filename : filename component only
---    - filepath : full path, identical to p
---    - root_dir : the root dir (home, dailies, ...)
---    - sub_dir : subdir if present, relative to root_dir
---    - is_periodic : bool
---    - is_daily : bool
---    - is_weekly : bool
---    - template : suggested template based on opts
local Pinfo = {
    fexists = false,
    title = "",
    filename = "",
    filepath = "",
    root_dir = "",
    sub_dir = "",
    is_periodic = false,
    is_daily = false,
    is_weekly = false,
    is_monthly = false,
    is_quarterly = false,
    is_yearly = false,
    template = "",
    calendar_info = nil,
}

function Pinfo:new(opts)
    opts = opts or {}

    local object = {}
    setmetatable(object, self)
    self.__index = self
    if opts.filepath then
        return object:resolve_path(opts.filepath, opts)
    end
    if opts.title ~= nil then
        return object:resolve_link(opts.title, opts)
    end
    return object
end

--- resolve_path(p, opts)
--- inspects the path and returns a Pinfo table
function Pinfo:resolve_path(p, opts)
    opts = opts or {}
    opts.subdirs_in_links = opts.subdirs_in_links or M.Cfg.subdirs_in_links

    self.fexists = fileutils.file_exists(p)
    self.filepath = p
    self.root_dir = M.Cfg.home
    self.is_periodic = false
    self.is_daily = false
    self.is_weekly = false
    self.is_monthly = false
    self.is_quarterly = false
    self.is_yearly = false

    -- strip all dirs to get filename
    local pp = Path:new(p)
    local p_splits = pp:_split()
    self.filename = p_splits[#p_splits]
    self.title = self.filename:gsub(M.Cfg.extension, "")

    if vim.startswith(p, M.Cfg.home) then
        self.root_dir = M.Cfg.home
    end
    if vim.startswith(p, M.Cfg.dailies) then
        self.root_dir = M.Cfg.dailies
        -- TODO: parse "title" into calendarinfo like in resolve_link
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        self.is_periodic = true
        self.is_daily = true
    end
    if vim.startswith(p, M.Cfg.weeklies) then
        -- TODO: parse "title" into calendarinfo like in resolve_link
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        self.root_dir = M.Cfg.weeklies
        self.is_periodic = true
        self.is_weekly = true
    end
    if vim.startswith(p, M.Cfg.monthlies) then
        -- TODO: parse "title" into calendarinfo like in resolve_link
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        self.root_dir = M.Cfg.monthlies
        self.is_periodic = true
        self.is_monthly = true
    end
    if vim.startswith(p, M.Cfg.quarterlies) then
        -- TODO: parse "title" into calendarinfo like in resolve_link
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        self.root_dir = M.Cfg.quarterlies
        self.is_periodic = true
        self.is_quarterly = true
    end
    if vim.startswith(p, M.Cfg.yearlies) then
        -- TODO: parse "title" into calendarinfo like in resolve_link
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        self.root_dir = M.Cfg.yearlies
        self.is_periodic = true
        self.is_yearly = true
    end

    -- now work out subdir relative to root
    self.sub_dir = p:gsub(tkutils.escape(self.root_dir .. "/"), "")
        :gsub(tkutils.escape(self.filename), "")
        :gsub("/$", "")
        :gsub("^/", "")

    if opts.subdirs_in_links and #self.sub_dir > 0 then
        self.title = self.sub_dir .. "/" .. self.title
    end

    return self
end

local function check_if_periodic(title)
    local daily_match = "^(%d%d%d%d)-(%d%d)-(%d%d)$"
    local weekly_match = "^(%d%d%d%d)-W(%d%d)$"
    local monthly_match = "^(%d%d%d%d)-(%d%d)$"
    local quarterly_match = "^(%d%d%d%d)-Q([1-4])$"
    local yearly_match = "^(%d%d%d%d)$"

    local is_daily = false
    local is_weekly = false
    local is_monthly = false
    local is_quarterly = false
    local is_yearly = false
    local dateinfo =
        dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday) -- sane default

    local start, _, year, month, day = title:find(daily_match)
    if start ~= nil then
        if tonumber(month) < 13 then
            if tonumber(day) < 32 then
                is_daily = true
                dateinfo.year = tonumber(year)
                dateinfo.month = tonumber(month)
                dateinfo.day = tonumber(day)
                dateinfo = dateutils.calculate_dates(
                    dateinfo,
                    M.Cfg.calendar_opts.calendar_monday
                )
            end
        end
    end

    local week
    start, _, year, week = title:find(weekly_match)
    if start ~= nil then
        if tonumber(week) < 53 then
            is_weekly = true
            -- ISO8601 week -> date calculation
            dateinfo = dateutils.isoweek_to_date(tonumber(year), tonumber(week))
            dateinfo = dateutils.calculate_dates(
                dateinfo,
                M.Cfg.calendar_opts.calendar_monday
            )
        end
    end

    start, _, year, month = title:find(monthly_match)
    if start ~= nil then
        if tonumber(month) > 0 and tonumber(month) < 13 then
            is_monthly = true
            dateinfo.year = tonumber(year)
            dateinfo.month = tonumber(month)
            -- Minimal Day anchor to avoid any calculation errors
            dateinfo.day = 1
            dateinfo = dateutils.calculate_dates(
                dateinfo,
                M.Cfg.calendar_opts.calendar_monday
            )
        end
    end

    local q
    start, _, year, q = title:find(quarterly_match)
    if start ~= nil then
        local qi = tonumber(q)
        if qi >= 1 and qi <= 4 then
            is_quarterly = true
            local first_month = (qi - 1) * 3 + 1
            dateinfo.year = tonumber(year)
            dateinfo.month = first_month
            dateinfo.day = 1
            dateinfo = dateutils.calculate_dates(
                dateinfo,
                M.Cfg.calendar_opts.calendar_monday
            )
        end
    end

    start, _, year = title:find(yearly_match)
    if start ~= nil then
        is_yearly = true
        dateinfo.year = tonumber(year)
        dateinfo.month = 1
        dateinfo.day = 1
        dateinfo = dateutils.calculate_dates(
            dateinfo,
            M.Cfg.calendar_opts.calendar_monday
        )
    end

    return is_daily, is_weekly, is_monthly, is_quarterly, is_yearly, dateinfo
end

function Pinfo:resolve_link(title, opts)
    opts = opts or {}
    opts.yearlies = opts.yearlies or M.Cfg.yearlies
    opts.quarterlies = opts.quarterlies or M.Cfg.quarterlies
    opts.monthlies = opts.monthlies or M.Cfg.monthlies
    opts.weeklies = opts.weeklies or M.Cfg.weeklies
    opts.dailies = opts.dailies or M.Cfg.dailies
    opts.home = opts.home or M.Cfg.home
    opts.extension = opts.extension or M.Cfg.extension
    opts.template_handling = opts.template_handling or M.Cfg.template_handling
    opts.new_note_location = opts.new_note_location or M.Cfg.new_note_location

    -- Handle absolute path links (external files)
    if opts.is_absolute_path and opts.absolute_path_title then
        -- Expand ~ to home directory
        local expanded = vim.fn.expand(opts.absolute_path_title)

        -- Add extension if not present
        if
            not expanded:match("%.md$")
            and not expanded:match(opts.extension .. "$")
        then
            expanded = expanded .. opts.extension
        end

        -- Set the path information
        self.filepath = expanded
        self.fexists = fileutils.file_exists(expanded)
        self.filename = vim.fn.fnamemodify(expanded, ":t")
        self.title = self.filename:gsub(opts.extension, "")
        self.root_dir = vim.fn.fnamemodify(expanded, ":h")
        self.is_daily_or_weekly = false
        self.is_daily = false
        self.is_weekly = false
        self.template = nil
        self.calendar_info = nil
        self.sub_dir = ""

        return self
    end

    self.fexists = false
    self.title = title
    self.filename = title .. opts.extension
    self.filename = self.filename:gsub("^%./", "") -- strip potential leading ./
    self.root_dir = opts.home
    self.is_periodic = false
    self.is_daily = false
    self.is_weekly = false
    self.is_monthly = false
    self.is_quarterly = false
    self.is_yearly = false
    self.template = nil
    self.calendar_info = nil

    if
        opts.yearlies
        and fileutils.file_exists(opts.yearlies .. "/" .. self.filename)
    then
        -- TODO: parse "title" into calendarinfo like below
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        -- if we still want calendar_info, just move the code for it out of `if self.fexists == false`.
        self.filepath = opts.yearlies .. "/" .. self.filename
        self.fexists = true
        self.root_dir = opts.yearlies
        self.is_periodic = true
        self.is_yearly = true
    end
    if
        opts.quarterlies
        and fileutils.file_exists(opts.quarterlies .. "/" .. self.filename)
    then
        -- TODO: parse "title" into calendarinfo like below
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        -- if we still want calendar_info, just move the code for it out of `if self.fexists == false`.
        self.filepath = opts.quarterlies .. "/" .. self.filename
        self.fexists = true
        self.root_dir = opts.quarterlies
        self.is_periodic = true
        self.is_quarterly = true
    end
    if
        opts.monthlies
        and fileutils.file_exists(opts.monthlies .. "/" .. self.filename)
    then
        -- TODO: parse "title" into calendarinfo like below
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        -- if we still want calendar_info, just move the code for it out of `if self.fexists == false`.
        self.filepath = opts.monthlies .. "/" .. self.filename
        self.fexists = true
        self.root_dir = opts.monthlies
        self.is_periodic = true
        self.is_monthly = true
    end
    if
        opts.weeklies
        and fileutils.file_exists(opts.weeklies .. "/" .. self.filename)
    then
        -- TODO: parse "title" into calendarinfo like below
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        -- if we still want calendar_info, just move the code for it out of `if self.fexists == false`.
        self.filepath = opts.weeklies .. "/" .. self.filename
        self.fexists = true
        self.root_dir = opts.weeklies
        self.is_periodic = true
        self.is_weekly = true
    end
    if
        opts.dailies
        and fileutils.file_exists(opts.dailies .. "/" .. self.filename)
    then
        -- TODO: parse "title" into calendarinfo like below
        -- not really necessary as the file exists anyway and therefore we don't need to instantiate a template
        -- if we still want calendar_info, just move the code for it out of `if self.fexists == false`.
        self.filepath = opts.dailies .. "/" .. self.filename
        self.fexists = true
        self.root_dir = opts.dailies
        self.is_periodic = true
        self.is_daily = true
    end
    if fileutils.file_exists(opts.home .. "/" .. self.filename) then
        self.filepath = opts.home .. "/" .. self.filename
        self.fexists = true
    end

    if self.fexists == false then
        -- now search for it in all subdirs
        local subdirs = scan.scan_dir(opts.home, { only_dirs = true })
        local tempfn
        for _, folder in pairs(subdirs) do
            tempfn = folder .. "/" .. self.filename
            -- [[testnote]]
            if fileutils.file_exists(tempfn) then
                self.filepath = tempfn
                self.fexists = true
                -- print("Found: " ..self.filename)
                break
            end
        end
    end

    -- if we just cannot find the note, check if it's a daily or weekly one
    if self.fexists == false then
        -- TODO: if we're not smart, we also shouldn't need to try to set the calendar info..?
        --       I bet someone will want the info in there, so let's put it in if possible
        _, _, _, _, _, self.calendar_info = check_if_periodic(self.title) -- will set today as default, so leave in!

        if opts.new_note_location == "smart" then
            self.filepath = opts.home .. "/" .. self.filename -- default
            self.is_daily, self.is_weekly, self.is_monthly, self.is_quarterly, self.is_yearly, self.calendar_info =
                check_if_periodic(self.title)
            if self.is_daily == true then
                self.root_dir = opts.dailies
                self.filepath = opts.dailies .. "/" .. self.filename
                self.is_periodic = true
            end
            if self.is_weekly == true then
                self.root_dir = opts.weeklies
                self.filepath = opts.weeklies .. "/" .. self.filename
                self.is_periodic = true
            end
            if self.is_monthly == true then
                self.root_dir = opts.monthlies
                self.filepath = opts.monthlies .. "/" .. self.filename
                self.is_periodic = true
            end
            if self.is_quarterly == true then
                self.root_dir = opts.quarterlies
                self.filepath = opts.quarterlies .. "/" .. self.filename
                self.is_periodic = true
            end
            if self.is_yearly == true then
                self.root_dir = opts.yearlies
                self.filepath = opts.yearlies .. "/" .. self.filename
                self.is_periodic = true
            end
        elseif opts.new_note_location == "same_as_current" then
            local cwd = vim.fn.expand("%:p")
            if #cwd > 0 then
                self.root_dir = Path:new(cwd):parent():absolute()
                if Path:new(self.root_dir):exists() then
                    -- check if potential subfolders in filename would end up in a non-existing directory
                    self.filepath = self.root_dir .. "/" .. self.filename
                    if not Path:new(self.filepath):parent():exists() then
                        print("Path " .. self.filepath .. " is invalid!")
                        -- self.filepath = opts.home .. "/" .. self.filename
                    end
                else
                    print("Path " .. self.root_dir .. " is invalid!")
                    -- self.filepath = opts.home .. "/" .. self.filename
                end
            else
                self.filepath = opts.home .. "/" .. self.filename
            end
        else
            -- default fn for creation
            self.filepath = opts.home .. "/" .. self.filename
        end

        -- final round, there still can be a subdir mess-up
        if not Path:new(self.filepath):parent():exists() then
            print("Path " .. self.filepath .. " is invalid!")
        end
    end

    -- now work out subdir relative to root
    self.sub_dir = self.filepath
        :gsub(tkutils.escape(self.root_dir .. "/"), "")
        :gsub(tkutils.escape(self.filename), "")
        :gsub("/$", "")
        :gsub("^/", "")

    -- now suggest a template based on opts
    self.template = M.note_type_templates.normal
    if opts.template_handling == "prefer_new_note" then
        self.template = M.note_type_templates.normal
    elseif opts.template_handling == "always_ask" then
        self.template = nil
    elseif opts.template_handling == "smart" then
        if self.is_daily then
            self.template = M.note_type_templates.daily
        elseif self.is_weekly then
            self.template = M.note_type_templates.weekly
        elseif self.is_monthly then
            self.template = M.note_type_templates.monthly
        elseif self.is_quarterly then
            self.template = M.note_type_templates.quarterly
        elseif self.is_yearly then
            self.template = M.note_type_templates.yearly
        else
            self.template = M.note_type_templates.normal
        end
    end

    return self
end

local function filter_filetypes(flist, ftypes)
    local new_fl = {}
    ftypes = ftypes or { M.Cfg.extension }

    local ftypeok = {}
    for _, ft in pairs(ftypes) do
        ftypeok[ft] = true
    end

    for _, fn in pairs(flist) do
        if ftypeok[fileutils.get_extension(fn)] then
            table.insert(new_fl, fn)
        end
    end
    return new_fl
end

local sourced_file = debug_utils.sourced_filepath()
M.base_directory = vim.fn.fnamemodify(sourced_file, ":h:h:h")
local media_files_base_directory = M.base_directory
    .. "/telescope-media-files.nvim"
local defaulter = utils.make_default_callable
local media_preview = defaulter(function(opts)
    local preview_cmd = ""
    if M.Cfg.media_previewer == "telescope-media-files" then
        preview_cmd = media_files_base_directory .. "/scripts/vimg"
    end

    if M.Cfg.media_previewer == "catimg-previewer" then
        preview_cmd = M.base_directory
            .. "/telekasten.nvim/scripts/catimg-previewer"
    end

    if vim.startswith(M.Cfg.media_previewer, "viu-previewer") then
        preview_cmd = M.base_directory
            .. "/telekasten.nvim/scripts/"
            .. M.Cfg.media_previewer
    end

    if vim.fn.executable(preview_cmd) == 0 then
        print("Previewer not found: " .. preview_cmd)
        return conf.file_previewer(opts)
    end
    return previewers.new_termopen_previewer({
        get_command = opts.get_command
            or function(entry)
                local tmp_table = vim.split(entry.value, "\t")
                local preview = opts.get_preview_window()
                opts.cwd = opts.cwd and vim.fn.expand(opts.cwd)
                    or vim.loop.cwd()
                if vim.tbl_isempty(tmp_table) then
                    return { "echo", "" }
                end
                print(tmp_table[1])
                return {
                    preview_cmd,
                    tmp_table[1],
                    preview.col,
                    preview.line + 1,
                    preview.width,
                    preview.height,
                }
            end,
    })
end, {})

-- note picker actions
local picker_actions = {}

-- find_files_sorted(opts)
-- like builtin.find_files, but:
--     - uses plenary.scan_dir synchronously instead of external jobs
--     - pre-sorts the file list in descending order (nice for dates, most recent first)
--     - filters for allowed file types by extension
--     - (also supports devicons)
--     - displays subdirs if necessary
--         - e.g. when searching for daily notes, no subdirs are displayed
--         - but when entering a date in find_notes, the daily/ and weekly/ subdirs are displayed
--     - optionally previews media (pdf, images, mp4, webm)
--         - this requires the telescope-media-files.nvim extension
local function find_files_sorted(opts)
    opts = opts or {}
    local search_pattern = opts.search_pattern or nil
    local search_depth = opts.search_depth or nil
    local scan_opts = { search_pattern = search_pattern, depth = search_depth }

    local file_list = scan.scan_dir(opts.cwd, scan_opts)
    local filter_extensions = opts.filter_extensions or M.Cfg.filter_extensions
    file_list = filter_filetypes(file_list, filter_extensions)
    local sort_option = opts.sort or "filename"
    if sort_option == "modified" then
        table.sort(file_list, function(a, b)
            return vim.fn.getftime(a) > vim.fn.getftime(b)
        end)
    else
        table.sort(file_list, function(a, b)
            return a > b
        end)
    end

    local counts = nil
    if opts.show_link_counts then
        counts = linkutils.generate_backlink_map(M.Cfg)
    end

    -- display with devicons
    local function iconic_display(display_entry)
        local display_opts = {
            path_display = function(_, e)
                return e:gsub(tkutils.escape(opts.cwd .. "/"), "")
            end,
        }

        local hl_group
        local display = utils.transform_path(display_opts, display_entry.value)

        display, hl_group =
            utils.transform_devicons(display_entry.value, display, false)

        if hl_group then
            return display, { { { 1, 3 }, hl_group } }
        else
            return display
        end
    end

    -- for media_files
    local popup_opts = {}
    opts.get_preview_window = function()
        return popup_opts.preview
    end

    -- local width = config.width
    --     or config.layout_config.width
    --     or config.layout_config[config.layout_strategy].width
    --     or vim.o.columns
    -- local telescope_win_width
    -- if width > 1 then
    --     telescope_win_width = width
    -- else
    --     telescope_win_width = math.floor(vim.o.columns * width)
    -- end
    local displayer = entry_display.create({
        separator = "",
        items = {
            { width = 4 },
            { width = 4 },
            { remaining = true },
        },
    })

    local function make_display(entry)
        local fn = entry.value
        local nlinks = counts.link_counts[fn] or 0
        local nbacks = counts.backlink_counts[fn] or 0

        if opts.show_link_counts then
            local display, hl = iconic_display(entry)

            return displayer({
                {
                    "L" .. tostring(nlinks),
                    function()
                        return {
                            { { 0, 1 }, "tkTagSep" },
                            { { 1, 3 }, "tkTag" },
                        }
                    end,
                },
                {
                    "B" .. tostring(nbacks),
                    function()
                        return {
                            { { 0, 1 }, "tkTagSep" },
                            { { 1, 3 }, "DevIconMd" },
                        }
                    end,
                },
                {
                    display,
                    function()
                        return hl
                    end,
                },
            })
        else
            return iconic_display(entry)
        end
    end

    local function entry_maker(entry)
        local iconic_entry = {}
        iconic_entry.value = entry
        iconic_entry.path = entry
        iconic_entry.ordinal = entry
        if opts.show_link_counts then
            iconic_entry.display = make_display
        else
            iconic_entry.display = iconic_display
        end
        return iconic_entry
    end

    local previewer = conf.file_previewer(opts)
    if opts.preview_type == "media" then
        previewer = media_preview.new(opts)
    end

    opts.attach_mappings = opts.attach_mappings
        or function(_, _)
            actions.select_default:replace(picker_actions.select_default)
        end

    local picker = pickers.new(opts, {
        finder = finders.new_table({
            results = file_list,
            entry_maker = entry_maker,
        }),
        sorter = conf.generic_sorter(opts),
        previewer = previewer,
    })

    -- local oc = picker.finder.close
    --
    -- picker.finder.close = function()
    --     print('on close')
    --     print(vim.inspect(picker:get_selection()))
    --     -- unfortunately, no way to tell if the selection was confirmed or
    --     -- canceled out
    --     oc()
    --     -- alternative: attach default mappings for <ESC> and <C-c>
    --     --       if anyone quits with q!, it's their fault
    -- end

    -- for media_files:
    local line_count = vim.o.lines - vim.o.cmdheight
    if vim.o.laststatus ~= 0 then
        line_count = line_count - 1
    end

    popup_opts = picker:get_window_options(vim.o.columns, line_count)

    picker:find()
end

picker_actions.post_open = function()
    if M.Cfg.auto_set_filetype then
        vim.cmd("set ft=telekasten")
    end
    if M.Cfg.auto_set_syntax then
        vim.cmd("set syntax=telekasten")
    end
end

picker_actions.select_default = function(prompt_bufnr)
    local ret = action_set.select(prompt_bufnr, "default")
    picker_actions.post_open()
    return ret
end

function picker_actions.close(opts)
    opts = opts or {}
    return function(prompt_bufnr)
        actions.close(prompt_bufnr)
        if opts.erase then
            if fileutils.file_exists(opts.erase_file) then
                vim.fn.delete(opts.erase_file)
            end
        end
    end
end

function picker_actions.paste_tag(opts)
    return function(prompt_bufnr)
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        vim.api.nvim_put({ selection.value.tag }, "", true, true)
        if opts.insert_after_inserting or opts.i then
            vim.api.nvim_feedkeys("A", "m", false)
        end
    end
end

function picker_actions.yank_tag(opts)
    return function(prompt_bufnr)
        opts = opts or {}
        if opts.close_after_yanking then
            actions.close(prompt_bufnr)
        end
        local selection = action_state.get_selected_entry()
        vim.fn.setreg('"', selection.value.tag)
        print("yanked " .. selection.value.tag)
    end
end

function picker_actions.paste_link(opts)
    opts = opts or {}
    opts.subdirs_in_links = opts.subdirs_in_links or M.Cfg.subdirs_in_links
    return function(prompt_bufnr)
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        local filepath = selection.filename or selection.value

        -- Check if file is external to current vault (auto-detect)
        local is_external = M.Cfg.external_link_follow
            and not vim.startswith(filepath, M.Cfg.home)

        local title
        if is_external then
            -- Use absolute path format for external files
            -- Remove extension and replace home directory with ~
            local link_path = filepath:gsub(M.Cfg.extension .. "$", "")
            local home = vim.fn.expand("~")
            if vim.startswith(link_path, home) then
                link_path = "~" .. link_path:sub(#home + 1)
            end
            title = "[[" .. link_path .. "]]"
        else
            -- Standard link within current vault
            local pinfo = Pinfo:new({
                filepath = filepath,
                opts,
            })
            title = "[[" .. pinfo.title .. "]]"
        end

        vim.api.nvim_put({ title }, "", true, true)
        if opts.insert_after_inserting or opts.i then
            vim.api.nvim_feedkeys("A", "m", false)
        end
    end
end

function picker_actions.yank_link(opts)
    return function(prompt_bufnr)
        opts = opts or {}
        opts.subdirs_in_links = opts.subdirs_in_links or M.Cfg.subdirs_in_links
        if opts.close_after_yanking then
            actions.close(prompt_bufnr)
        end
        local selection = action_state.get_selected_entry()
        local filepath = selection.filename or selection.value

        -- Check if file is external to current vault (auto-detect)
        local is_external = M.Cfg.external_link_follow
            and not vim.startswith(filepath, M.Cfg.home)

        local title
        if is_external then
            -- Use absolute path format for external files
            -- Remove extension and replace home directory with ~
            local link_path = filepath:gsub(M.Cfg.extension .. "$", "")
            local home = vim.fn.expand("~")
            if vim.startswith(link_path, home) then
                link_path = "~" .. link_path:sub(#home + 1)
            end
            title = "[[" .. link_path .. "]]"
        else
            -- Standard link within current vault
            local pinfo = Pinfo:new({
                filepath = filepath,
                opts,
            })
            title = "[[" .. pinfo.title .. "]]"
        end

        vim.fn.setreg('"', title)
        print("yanked " .. title)
    end
end

function picker_actions.paste_img_link(opts)
    return function(prompt_bufnr)
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        local fn = selection.value
        fn = make_relative_path(vim.fn.expand("%:p"), fn, "/")
        local imglink = "![](" .. fn .. ")"
        vim.api.nvim_put({ imglink }, "", true, true)
        if opts.insert_after_inserting or opts.i then
            vim.api.nvim_feedkeys("A", "m", false)
        end
    end
end

function picker_actions.yank_img_link(opts)
    return function(prompt_bufnr)
        opts = opts or {}
        if opts.close_after_yanking then
            actions.close(prompt_bufnr)
        end
        local selection = action_state.get_selected_entry()
        local fn = selection.value
        fn = make_relative_path(vim.fn.expand("%:p"), fn, "/")
        local imglink = "![](" .. fn .. ")"
        vim.fn.setreg('"', imglink)
        print("yanked " .. imglink)
    end
end

--
-- FindDailyNotes:
-- ---------------
--
-- Select from daily notes
--
local function FindDailyNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local today = os.date(dateutils.dateformats.date)
        local fname = M.Cfg.dailies .. "/" .. today .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            find_files_sorted({
                prompt_title = "Find daily note",
                cwd = M.Cfg.dailies,
                find_command = M.Cfg.find_command,
                search_pattern = "%d%d%d%d%-%d%d%-%d%d",
                search_depth = 1,
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        end
        if
            (fexists ~= true)
            and (
                (opts.dailies_create_nonexisting == true)
                or M.Cfg.dailies_create_nonexisting == true
            )
        then
            create_note_from_template(
                today,
                nil,
                fname,
                M.note_type_templates.daily,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end
        picker()
    end)
end

--
-- FindWeeklyNotes:
-- ---------------
--
-- Select from weekly notes
--
local function FindWeeklyNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local title = os.date(dateutils.dateformats.isoweek)
        local fname = M.Cfg.weeklies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)

        local function picker()
            find_files_sorted({
                prompt_title = "Find weekly note",
                cwd = M.Cfg.weeklies,
                find_command = M.Cfg.find_command,
                search_pattern = "%d%d%d%d%-W%d+",
                search_depth = 1,
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        end

        if
            (fexists ~= true)
            and (
                (opts.weeklies_create_nonexisting == true)
                or M.Cfg.weeklies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.weekly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end
        picker()
    end)
end

--
-- FindMonthlyNotes:
-- ---------------
--
-- Select from monthly notes
--
--
local function FindMonthlyNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local title = os.date(dateutils.dateformats.month_ym)
        local fname = M.Cfg.monthlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            find_files_sorted({
                prompt_title = "Find monthly note",
                cwd = M.Cfg.monthlies,
                find_command = M.Cfg.find_command,
                search_pattern = "%d%d%d%d%-%d%d"
                    .. vim.pesc(M.Cfg.extension)
                    .. "$",
                search_depth = 1,
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        end
        if
            (fexists ~= true)
            and (
                (opts.monthlies_create_nonexisting == true)
                or M.Cfg.monthlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.monthly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end
        picker()
    end)
end

--
-- FindQuarterlyNotes:
-- ---------------
--
-- Select from quarterly notes
--
--
local function FindQuarterlyNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        -- Use direct value instead of dateformat.quarter_yq, because os.date doesn't properly expand to a date
        local dinfo =
            dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday)
        local title = dinfo.quarter_yq
        local fname = M.Cfg.quarterlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            find_files_sorted({
                prompt_title = "Find quarterly note",
                cwd = M.Cfg.quarterlies,
                find_command = M.Cfg.find_command,
                search_pattern = "%d%d%d%d%-Q[1-4]" .. vim.pesc(
                    M.Cfg.extension
                ) .. "$",
                search_depth = 1,
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        end
        if
            (fexists ~= true)
            and (
                (opts.quarterlies_create_nonexisting == true)
                or M.Cfg.quarterlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.quarterly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end
        picker()
    end)
end

--
-- FindYearlyNotes:
-- ---------------
--
-- Select from yearly notes
--
--
local function FindYearlyNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local title = os.date(dateutils.dateformats.year)
        local fname = M.Cfg.yearlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            find_files_sorted({
                prompt_title = "Find yearly note",
                cwd = M.Cfg.yearlies,
                find_command = M.Cfg.find_command,
                search_pattern = "%d%d%d%d" .. vim.pesc(M.Cfg.extension) .. "$",
                search_depth = 1,
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        end
        if
            (fexists ~= true)
            and (
                (opts.yearlies_create_nonexisting == true)
                or M.Cfg.yearlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.yearly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end
        picker()
    end)
end

-- InsertLink:
-- -----------
--
-- Select from all notes and put a link in the current buffer
--
local function InsertLink(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.subdirs_in_links = opts.subdirs_in_links or M.Cfg.subdirs_in_links

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local cwd = M.Cfg.home
        local find_command = M.Cfg.find_command
        local sort = M.Cfg.sort
        local attach_mappings = function(prompt_bufnr, map)
            actions.select_default:replace(function()
                actions.close(prompt_bufnr)
                local selection = action_state.get_selected_entry()
                if selection == nil then
                    selection = { filename = action_state.get_current_line() }
                end
                local pinfo = Pinfo:new({
                    filepath = selection.filename or selection.value,
                    opts,
                })
                vim.api.nvim_put(
                    { "[[" .. pinfo.title .. "]]" },
                    "",
                    false,
                    true
                )
                if opts.i then
                    vim.api.nvim_feedkeys("a", "m", false)
                end
            end)
            map("i", "<c-y>", picker_actions.yank_link(opts))
            map("i", "<c-i>", picker_actions.paste_link(opts))
            map("n", "<c-y>", picker_actions.yank_link(opts))
            map("n", "<c-i>", picker_actions.paste_link(opts))
            map("i", "<c-cr>", picker_actions.paste_link(opts))
            map("n", "<c-cr>", picker_actions.paste_link(opts))
            return true
        end

        if opts.with_live_grep then
            builtin.live_grep({
                prompt_title = "Insert link to note with live grep",
                cwd = cwd,
                attach_mappings = attach_mappings,
                find_command = find_command,
                sort = sort,
            })
        else
            find_files_sorted({
                prompt_title = "Insert link to note",
                cwd = cwd,
                attach_mappings = attach_mappings,
                find_command = find_command,
                sort = sort,
            })
        end
    end)
end

-- local function check_for_link_or_tag()
local function check_for_link_or_tag()
    local line = vim.api.nvim_get_current_line()
    local col = vim.fn.col(".")
    return taglinks.is_tag_or_link_at(line, col, M.Cfg)
end

local function follow_url(url)
    if M.Cfg.follow_url_fallback then
        local cmd = string.gsub(M.Cfg.follow_url_fallback, "{{url}}", url)
        return vim.cmd(cmd)
    end

    -- we just leave it to the OS's handler to deal with what kind of URL it is
    local function format_command(cmd)
        return 'call jobstart(["'
            .. cmd
            .. '", "'
            .. url
            .. '"], {"detach": v:true})'
    end

    local command
    if vim.fn.has("mac") == 1 then
        command = format_command("open")
        vim.cmd(command)
    elseif vim.fn.has("unix") then
        command = format_command("xdg-open")
        vim.cmd(command)
    else
        print("Cannot open URLs on your operating system")
    end
end

--
-- PreviewImg:
-- -----------
--
-- preview media
--
local function PreviewImg(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local saved_reg = vim.fn.getreg('"0')
        vim.cmd("normal yi)")
        local fname = vim.fn.getreg('"0'):gsub("^img/", "")
        vim.fn.setreg('"0', saved_reg)

        -- check if fname exists anywhere
        local imageDir = M.Cfg.image_subdir or M.Cfg.home
        local fexists = fileutils.file_exists(imageDir .. "/" .. fname)

        if fexists == true then
            find_files_sorted({
                prompt_title = "Preview image/media",
                cwd = imageDir,
                default_text = fname,
                find_command = M.Cfg.find_command,
                filter_extensions = M.Cfg.media_extensions,
                preview_type = "media",
                attach_mappings = function(prompt_bufnr, map)
                    actions.select_default:replace(function()
                        actions.close(prompt_bufnr)
                    end)
                    map("i", "<c-y>", picker_actions.yank_img_link(opts))
                    map("i", "<c-i>", picker_actions.paste_img_link(opts))
                    map("n", "<c-y>", picker_actions.yank_img_link(opts))
                    map("n", "<c-i>", picker_actions.paste_img_link(opts))
                    map("i", "<c-cr>", picker_actions.paste_img_link(opts))
                    map("n", "<c-cr>", picker_actions.paste_img_link(opts))
                    return true
                end,
                sort = M.Cfg.sort,
            })
        else
            print("File not found: " .. M.Cfg.home .. "/" .. fname)
        end
    end)
end

--
-- BrowseImg:
-- -----------
--
-- preview media
--
local function BrowseImg(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        find_files_sorted({
            prompt_title = "Preview image/media",
            cwd = M.Cfg.home,
            find_command = M.Cfg.find_command,
            filter_extensions = M.Cfg.media_extensions,
            preview_type = "media",
            attach_mappings = function(prompt_bufnr, map)
                actions.select_default:replace(function()
                    actions.close(prompt_bufnr)
                end)
                map("i", "<c-y>", picker_actions.yank_img_link(opts))
                map("i", "<c-i>", picker_actions.paste_img_link(opts))
                map("n", "<c-y>", picker_actions.yank_img_link(opts))
                map("n", "<c-i>", picker_actions.paste_img_link(opts))
                map("i", "<c-cr>", picker_actions.paste_img_link(opts))
                map("n", "<c-cr>", picker_actions.paste_img_link(opts))
                return true
            end,
            sort = M.Cfg.sort,
        })
    end)
end

--
-- FindFriends:
-- -----------
--
-- Find notes also linking to the link under cursor
--
local function FindFriends(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local saved_reg = vim.fn.getreg('"0')
        vim.cmd("normal yi]")
        local title = vim.fn.getreg('"0')
        vim.fn.setreg('"0', saved_reg)

        title = linkutils.remove_alias(title)
        title = title:gsub("^(%[)(.+)(%])$", "%2")

        builtin.live_grep({
            prompt_title = "Notes referencing `" .. title .. "`",
            cwd = M.Cfg.home,
            default_text = "\\[\\[" .. title .. "([#|].+)?\\]\\]",
            find_command = M.Cfg.find_command,
            attach_mappings = function(_, map)
                actions.select_default:replace(picker_actions.select_default)
                map("i", "<c-y>", picker_actions.yank_link(opts))
                map("i", "<c-i>", picker_actions.paste_link(opts))
                map("n", "<c-y>", picker_actions.yank_link(opts))
                map("n", "<c-i>", picker_actions.paste_link(opts))
                map("i", "<c-cr>", picker_actions.paste_link(opts))
                map("n", "<c-cr>", picker_actions.paste_link(opts))
                return true
            end,
        })
    end)
end

--
-- YankLink:
-- -----------
--
-- Create and yank a [[link]] from the current note.
--
local function YankLink()
    local title = "[["
        .. Pinfo:new({ filepath = vim.fn.expand("%:p"), M.Cfg }).title
        .. "]]"
    vim.fn.setreg('"', title)
    print("yanked " .. title)
end

local function rename_update_links(oldfile, newname)
    if M.Cfg.rename_update_links == true then
        print(
            "Updating links from '"
                .. oldfile.title
                .. "' to '"
                .. newname
                .. "'"
        )

        -- Save open buffers before looking for links to replace
        if #(vim.fn.getbufinfo({ bufmodified = 1 })) > 1 then
            vim.ui.select({ "Yes (default)", "No" }, {
                prompt = "Telekasten.nvim: "
                    .. "Save all modified buffers before updating links?",
            }, function(answer)
                if answer ~= "No" then
                    save_all_mod_buffers()
                end
            end)
        end

        -- Update links in all directories
        recursive_substitution(M.Cfg.home, oldfile.title, newname)
        recursive_substitution(M.Cfg.dailies, oldfile.title, newname)
        recursive_substitution(M.Cfg.weeklies, oldfile.title, newname)
        recursive_substitution(M.Cfg.monthlies, oldfile.title, newname)
        recursive_substitution(M.Cfg.quarterlies, oldfile.title, newname)
        recursive_substitution(M.Cfg.yearlies, oldfile.title, newname)

        print("Link update completed!")
    end
end

--
-- RenameNote:
-- -----------
--
-- Prompt for new note title, rename the note and update all links.
--
local function RenameNote()
    local oldfile = Pinfo:new({ filepath = vim.fn.expand("%:p"), M.Cfg })

    local oldtitle = oldfile.title
    local curuuid = ""
    local escapedsep = M.Cfg.uuid_sep:gsub("[%(%)%.%%%+%-%*%?%[%^%$%]]", "%%%1")

    if M.Cfg.new_note_filename == "uuid-title" then
        curuuid, oldtitle =
            oldfile.title:match("^(.-)" .. escapedsep .. "(.+)$")
    elseif M.Cfg.new_note_filename == "title-uuid" then
        oldtitle, curuuid =
            oldfile.title:match("^(.+)" .. escapedsep .. "(.-)$")
    end

    fileutils.prompt_title(M.Cfg.extension, oldtitle, function(newname)
        newname = generate_note_filename(curuuid, newname)
        local newpath = newname:match("(.*/)") or ""
        newpath = M.Cfg.home .. "/" .. newpath

        -- If no subdir specified, place the new note in the same place as old note
        if
            M.Cfg.subdirs_in_links == true
            and newpath == M.Cfg.home .. "/"
            and oldfile.sub_dir ~= ""
        then
            newname = oldfile.sub_dir .. "/" .. newname
        end

        local fname = M.Cfg.home .. "/" .. newname .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        if fexists then
            tkutils.print_error("File already exists. Renaming abandoned")
            return
        end

        -- Save newfile, delete buffer of old one and remove old file
        if newname ~= "" and newname ~= oldfile.title then
            check_dir_and_ask(newpath, "Renamed file", function(success)
                if not success then
                    return
                end

                -- Update the current buffer's content to reflect the new title
                local current_content = vim.fn.getline(1, "$")
                local updated_content = {}

                for _, line in ipairs(current_content) do
                    -- Update frontmatter title field
                    if line:match("^title:") then
                        table.insert(updated_content, "title: " .. newname)
                    else
                        table.insert(updated_content, line)
                    end
                end

                -- Write the updated content to the current buffer
                vim.fn.setline(1, updated_content)

                local oldTitle = oldfile.title:gsub(" ", "\\ ")
                vim.cmd(
                    "saveas " .. M.Cfg.home .. "/" .. newname .. M.Cfg.extension
                )
                vim.cmd("bdelete " .. oldTitle .. M.Cfg.extension)
                os.execute(
                    "rm " .. M.Cfg.home .. "/" .. oldTitle .. M.Cfg.extension
                )
                rename_update_links(oldfile, newname)
            end)
        else
            rename_update_links(oldfile, newname)
        end
    end)
end

--
-- GotoDate:
-- ----------
--
-- find note for date and create it if necessary.
--
local function GotoDate(opts)
    opts.dates = dateutils.calculate_dates(
        opts.date_table,
        M.Cfg.calendar_opts.calendar_monday
    )
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.journal_auto_open = opts.journal_auto_open or M.Cfg.journal_auto_open

    local word = opts.date or os.date(dateutils.dateformats.date)

    local fname = M.Cfg.dailies .. "/" .. word .. M.Cfg.extension
    local fexists = fileutils.file_exists(fname)
    local function picker()
        if opts.journal_auto_open then
            if opts.calendar == true then
                -- ensure that the calendar window is not improperly overwritten
                vim.cmd("wincmd w")
            end
            vim.cmd("e " .. fname)
        else
            find_files_sorted({
                prompt_title = "Goto day",
                cwd = M.Cfg.dailies,
                default_text = word,
                find_command = M.Cfg.find_command,
                attach_mappings = function(prompt_bufnr, map)
                    actions.select_default:replace(function()
                        actions.close(prompt_bufnr)

                        -- open the new note
                        if opts.calendar == true then
                            vim.cmd("wincmd w")
                        end
                        vim.cmd("e " .. fname)
                        picker_actions.post_open()
                    end)
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
            })
        end
    end

    if
        (fexists ~= true)
        and (
            (opts.dailies_create_nonexisting == true)
            or M.Cfg.dailies_create_nonexisting == true
        )
    then
        create_note_from_template(
            word,
            nil,
            fname,
            M.note_type_templates.daily,
            opts.dates,
            function()
                opts.erase = true
                opts.erase_file = fname
                picker()
            end
        )
        return
    end

    picker()
end

--
-- GotoToday:
-- ----------
--
-- find today's daily note and create it if necessary.
--
local function GotoToday(opts)
    opts = opts or {}

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local today = os.date(dateutils.dateformats.date)
        opts.date_table = os.date("*t")
        opts.date = today
        opts.dailies_create_nonexisting = true -- Always use template for GotoToday
        GotoDate(opts)
    end)
end

--
-- FindNotes:
-- ----------
--
-- Select from notes
--
local function FindNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local cwd = M.Cfg.home
        local find_command = M.Cfg.find_command
        local sort = M.Cfg.sort
        local attach_mappings = function(_, map)
            actions.select_default:replace(picker_actions.select_default)
            map("i", "<c-y>", picker_actions.yank_link(opts))
            map("i", "<c-i>", picker_actions.paste_link(opts))
            map("n", "<c-y>", picker_actions.yank_link(opts))
            map("n", "<c-i>", picker_actions.paste_link(opts))
            map("i", "<c-cr>", picker_actions.paste_link(opts))
            map("n", "<c-cr>", picker_actions.paste_link(opts))
            if M.Cfg.enable_create_new then
                map("i", "<c-n>", picker_actions.create_new(opts))
                map("n", "<c-n>", picker_actions.create_new(opts))
            end
            return true
        end

        if opts.with_live_grep then
            builtin.live_grep({
                prompt_title = "Find notes by live grep",
                cwd = cwd,
                find_command = find_command,
                attach_mappings = attach_mappings,
                sort = sort,
            })
        else
            find_files_sorted({
                prompt_title = "Find notes by name",
                cwd = cwd,
                find_command = find_command,
                attach_mappings = attach_mappings,
                sort = sort,
            })
        end
    end)
end

--
-- InsertImgLink:
-- --------------
--
-- Insert link to image / media, with optional preview
--
local function InsertImgLink(opts)
    opts = opts or {}

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        find_files_sorted({
            prompt_title = "Find image/media",
            cwd = M.Cfg.home,
            find_command = M.Cfg.find_command,
            filter_extensions = M.Cfg.media_extensions,
            preview_type = "media",
            attach_mappings = function(prompt_bufnr, map)
                actions.select_default:replace(function()
                    actions.close(prompt_bufnr)
                    local selection = action_state.get_selected_entry()
                    local fn = selection.value
                    fn = make_relative_path(vim.fn.expand("%:p"), fn, "/")
                    vim.api.nvim_put({ "![](" .. fn .. ")" }, "", true, true)
                    if opts.i then
                        vim.api.nvim_feedkeys("A", "m", false)
                    end
                end)
                map("i", "<c-y>", picker_actions.yank_img_link(opts))
                map("i", "<c-i>", picker_actions.paste_img_link(opts))
                map("n", "<c-y>", picker_actions.yank_img_link(opts))
                map("n", "<c-i>", picker_actions.paste_img_link(opts))
                map("i", "<c-cr>", picker_actions.paste_img_link(opts))
                map("n", "<c-cr>", picker_actions.paste_img_link(opts))
                return true
            end,
            sort = M.Cfg.sort,
        })
    end)
end

--
-- SearchNotes:
-- ------------
--
-- find the file linked to by the word under the cursor
--
local function SearchNotes(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        builtin.live_grep({
            prompt_title = "Search in notes",
            cwd = M.Cfg.home,
            search_dirs = { M.Cfg.home },
            default_text = opts.default_text or vim.fn.expand("<cword>"),
            find_command = M.Cfg.find_command,
            attach_mappings = function(_, map)
                actions.select_default:replace(picker_actions.select_default)
                map("i", "<c-y>", picker_actions.yank_link(opts))
                map("i", "<c-i>", picker_actions.paste_link(opts))
                map("n", "<c-y>", picker_actions.yank_link(opts))
                map("n", "<c-i>", picker_actions.paste_link(opts))
                map("i", "<c-cr>", picker_actions.paste_link(opts))
                map("n", "<c-cr>", picker_actions.paste_link(opts))
                return true
            end,
        })
    end)
end

--
-- ShowBacklinks:
-- ------------
--
-- Find all notes linking to this one
--
local function ShowBacklinks(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local title =
            Pinfo:new({ filepath = vim.fn.expand("%:p"), M.Cfg }).title
        -- or vim.api.nvim_buf_get_name(0)

        local escaped_title = string.gsub(title, "%(", "\\(")
        escaped_title = string.gsub(escaped_title, "%)", "\\)")

        builtin.live_grep({
            results_title = "Backlinks to " .. title,
            prompt_title = "Search",
            cwd = M.Cfg.home,
            search_dirs = { M.Cfg.home },
            default_text = "\\[\\[" .. escaped_title .. "([#|].+)?\\]\\]",
            find_command = M.Cfg.find_command,
            attach_mappings = function(_, map)
                actions.select_default:replace(picker_actions.select_default)
                map("i", "<c-y>", picker_actions.yank_link(opts))
                map("i", "<c-i>", picker_actions.paste_link(opts))
                map("n", "<c-y>", picker_actions.yank_link(opts))
                map("n", "<c-i>", picker_actions.paste_link(opts))
                map("i", "<c-cr>", picker_actions.paste_link(opts))
                map("n", "<c-cr>", picker_actions.paste_link(opts))
                return true
            end,
        })
    end)
end

--
-- CreateNoteSelectTemplate()
-- --------------------------
--
-- Prompts for title, then pops up telescope for template selection,
-- creates the new note by template and opens it

local function on_create_with_template(opts, title)
    if title == nil then
        return
    end

    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.new_note_location = opts.new_note_location or M.Cfg.new_note_location
    opts.template_handling = opts.template_handling or M.Cfg.template_handling
    local uuid_type = opts.uuid_type or M.Cfg.uuid_type

    local uuid = fileutils.new_uuid(uuid_type)
    local pinfo = Pinfo:new({
        title = generate_note_filename(uuid, title),
        opts,
    })
    local fname = pinfo.filepath
    if pinfo.fexists == true then
        -- open the new note
        vim.cmd("e " .. fname)
        picker_actions.post_open()
        return
    end

    find_files_sorted({
        prompt_title = "Select template...",
        cwd = M.Cfg.templates,
        find_command = M.Cfg.find_command,
        attach_mappings = function(prompt_bufnr, map)
            actions.select_default:replace(function()
                actions.close(prompt_bufnr)
                -- local template = M.Cfg.templates .. "/" .. action_state.get_selected_entry().value
                local template = action_state.get_selected_entry().value
                -- TODO: pass in the calendar_info returned from the pinfo
                create_note_from_template(
                    title,
                    uuid,
                    fname,
                    template,
                    pinfo.calendar_info,
                    function()
                        -- open the new note
                        vim.cmd("e " .. fname)
                        picker_actions.post_open()
                    end
                )
            end)
            map("i", "<c-y>", picker_actions.yank_link(opts))
            map("i", "<c-i>", picker_actions.paste_link(opts))
            map("n", "<c-y>", picker_actions.yank_link(opts))
            map("n", "<c-i>", picker_actions.paste_link(opts))
            return true
        end,
    })
end

local function CreateNoteSelectTemplate(opts)
    opts = opts or {}

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        fileutils.prompt_title(M.Cfg.extension, nil, function(title)
            on_create_with_template(opts, title)
        end, M.Cfg.home)
    end)
end

--
-- CreateNote:
-- ------------
--
-- Prompts for title and creates note with default template
--
local function on_create(opts, title)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.new_note_location = opts.new_note_location or M.Cfg.new_note_location
    opts.template_handling = opts.template_handling or M.Cfg.template_handling
    local uuid_type = opts.uuid_type or M.Cfg.uuid_type

    if title == nil then
        return
    end

    local uuid = fileutils.new_uuid(uuid_type)
    local pinfo = Pinfo:new({
        title = generate_note_filename(uuid, title),
        opts,
    })
    local fname = pinfo.filepath

    local function picker()
        find_files_sorted({
            prompt_title = "Created note...",
            cwd = pinfo.root_dir,
            default_text = generate_note_filename(uuid, title),
            find_command = M.Cfg.find_command,
            attach_mappings = function(_, map)
                actions.select_default:replace(picker_actions.select_default)
                map("i", "<c-y>", picker_actions.yank_link(opts))
                map("i", "<c-i>", picker_actions.paste_link(opts))
                map("n", "<c-y>", picker_actions.yank_link(opts))
                map("n", "<c-i>", picker_actions.paste_link(opts))
                map("n", "<c-c>", picker_actions.close(opts))
                map("n", "<esc>", picker_actions.close(opts))
                return true
            end,
        })
    end
    if pinfo.fexists ~= true then
        -- TODO: pass in the calendar_info returned in pinfo
        create_note_from_template(
            title,
            uuid,
            fname,
            pinfo.template,
            pinfo.calendar_info,
            function()
                opts.erase = true
                opts.erase_file = fname
                picker()
            end
        )
        return
    end

    picker()
end

local function CreateNote(opts)
    opts = opts or {}

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        if M.Cfg.template_handling == "always_ask" then
            return CreateNoteSelectTemplate(opts)
        end

        fileutils.prompt_title(M.Cfg.extension, nil, function(title)
            on_create(opts, title)
        end, M.Cfg.home)
    end)
end

--
-- FollowLink:
-- -----------
--
-- find the file linked to by the word under the cursor
--
local function FollowLink(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking

    opts.template_handling = opts.template_handling or M.Cfg.template_handling
    opts.new_note_location = opts.new_note_location or M.Cfg.new_note_location
    local uuid_type = opts.uuid_type or M.Cfg.uuid_type

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local search_mode = "files"
        local title
        local filename_part = ""

        -- first: check if we're in a tag or a link
        local kind
        local globArg = ""

        if opts.follow_tag ~= nil then
            kind = "tag"
            title = opts.follow_tag
            if opts.templateDir ~= nil then
                globArg = "--glob=!" .. "**/" .. opts.templateDir .. "/*.md"
            end
        else
            kind, _ = check_for_link_or_tag()
        end

        if kind == "tag" then
            search_mode = "tag"
            if title == nil then
                local saved_reg = vim.fn.getreg('"0')
                vim.cmd("normal yiw")
                title = vim.fn.getreg('"0')
                vim.fn.setreg('"0', saved_reg)
            end
        else
            local saved_reg = vim.fn.getreg('"0')
            if kind == "link" then
                -- we are in a link
                vim.cmd("normal yi]")
                title = vim.fn.getreg('"0')
                title = title:gsub("^(%[)(.+)(%])$", "%2")
                title = title:gsub("%s*\n", " ")
                title = linkutils.remove_alias(title)

                -- Check if this is an absolute path link (external file)
                if
                    M.Cfg.external_link_follow
                    and (
                        title:match("^~/")
                        or title:match("^/")
                        or title:match("^%a:")
                    )
                then
                    -- This is an absolute path link to an external file
                    vim.fn.setreg('"0', saved_reg)

                    -- Handle absolute path immediately
                    local external_opts = vim.tbl_extend("force", opts, {
                        is_absolute_path = true,
                        absolute_path_title = title,
                        title = title,
                    })
                    local pinfo = Pinfo:new(external_opts)

                    if pinfo.fexists then
                        -- File exists, open it
                        vim.cmd("e " .. vim.fn.fnameescape(pinfo.filepath))
                    else
                        -- File doesn't exist (read-only mode for external files)
                        print("External file not found: " .. pinfo.filepath)
                    end
                    return
                end
            else
                -- we are in an external [link]
                vim.cmd("normal yi)")
                local url = vim.fn.getreg('"0')
                vim.fn.setreg('"0', saved_reg)
                return follow_url(url)
            end
            vim.fn.setreg('"0', saved_reg)

            local parts = vim.split(title, "#")

            -- if there is a #
            if #parts ~= 1 then
                search_mode = "heading"
                title = parts[2]
                filename_part = parts[1]
                parts = vim.split(title, "%^")
                if #parts ~= 1 then
                    search_mode = "para"
                    title = parts[2]
                end
            end

            -- this applies to heading and para search_mode
            -- if we cannot find the file, revert to global heading search by
            -- setting filename to empty string
            if #filename_part > 0 then
                local pinfo = Pinfo:new({ title = filename_part })
                filename_part = pinfo.filepath
                if pinfo.fexists == false then
                    -- print("error")
                    filename_part = ""
                end
            end
        end

        if search_mode == "files" then
            -- check if subdir exists
            local filepath = title:match("(.*/)") or ""
            filepath = M.Cfg.home .. "/" .. filepath
            check_dir_and_ask(filepath, "", function()
                -- check if fname exists anywhere
                local pinfo = Pinfo:new({ title = title })
                local function picker()
                    find_files_sorted({
                        prompt_title = "Follow link to note...",
                        cwd = pinfo.root_dir,
                        default_text = title,
                        find_command = M.Cfg.find_command,
                        attach_mappings = function(_, map)
                            actions.select_default:replace(
                                picker_actions.select_default
                            )
                            map("i", "<c-y>", picker_actions.yank_link(opts))
                            map("i", "<c-i>", picker_actions.paste_link(opts))
                            map("n", "<c-y>", picker_actions.yank_link(opts))
                            map("n", "<c-i>", picker_actions.paste_link(opts))
                            map("n", "<c-c>", picker_actions.close(opts))
                            map("n", "<esc>", picker_actions.close(opts))
                            return true
                        end,
                        sort = M.Cfg.sort,
                    })
                end

                if
                    (pinfo.fexists ~= true)
                    and (
                        (opts.follow_creates_nonexisting == true)
                        or M.Cfg.follow_creates_nonexisting == true
                    )
                then
                    if opts.template_handling == "always_ask" then
                        return on_create_with_template(opts, title)
                    end

                    if #pinfo.filepath > 0 then
                        local uuid = fileutils.new_uuid(uuid_type)
                        create_note_from_template(
                            title,
                            uuid,
                            pinfo.filepath,
                            pinfo.template,
                            pinfo.calendar_info,
                            function()
                                opts.erase = true
                                opts.erase_file = pinfo.filepath
                                picker()
                            end
                        )
                        return
                    end
                end

                picker()
            end)
        end

        if search_mode ~= "files" then
            local search_pattern = title
            local cwd = M.Cfg.home

            opts.cwd = cwd
            local counts = nil
            if opts.show_link_counts then
                counts = linkutils.generate_backlink_map(M.Cfg)
            end

            -- display with devicons
            local function iconic_display(display_entry)
                local display_opts = {
                    path_display = function(_, e)
                        return e:gsub(tkutils.escape(opts.cwd .. "/"), "")
                    end,
                }

                local hl_group
                local display =
                    utils.transform_path(display_opts, display_entry.value)

                display_entry.filn = display_entry.filn
                    or display:gsub(":.*", "")
                display, hl_group =
                    utils.transform_devicons(display_entry.filn, display, false)

                if hl_group then
                    return display, { { { 1, 3 }, hl_group } }
                else
                    return display
                end
            end

            -- for media_files
            local popup_opts = {}
            opts.get_preview_window = function()
                return popup_opts.preview
            end

            -- local width = config.width
            --     or config.layout_config.width
            --     or config.layout_config[config.layout_strategy].width
            --     or vim.o.columns
            -- local telescope_win_width
            -- if width > 1 then
            --     telescope_win_width = width
            -- else
            --     telescope_win_width = math.floor(vim.o.columns * width)
            -- end
            local displayer = entry_display.create({
                separator = "",
                items = {
                    { width = 4 },
                    { width = 4 },
                    { remaining = true },
                },
            })

            local function make_display(entry)
                local fn = entry.filename
                local nlinks = counts.link_counts[fn] or 0
                local nbacks = counts.backlink_counts[fn] or 0

                if opts.show_link_counts then
                    local display, hl = iconic_display(entry)

                    return displayer({
                        {
                            "L" .. tostring(nlinks),
                            function()
                                return {
                                    { { 0, 1 }, "tkTagSep" },
                                    { { 1, 3 }, "tkTag" },
                                }
                            end,
                        },
                        {
                            "B" .. tostring(nbacks),
                            function()
                                return {
                                    { { 0, 1 }, "tkTagSep" },
                                    { { 1, 3 }, "DevIconMd" },
                                }
                            end,
                        },
                        {
                            display,
                            function()
                                return hl
                            end,
                        },
                    })
                else
                    return iconic_display(entry)
                end
            end

            local lookup_keys = {
                value = 1,
                ordinal = 1,
            }

            local find = (function()
                if Path.path.sep == "\\" then
                    return function(t)
                        local start, _, filn, lnum, col, text =
                            string.find(t, [[([^:]+):(%d+):(%d+):(.*)]])

                        -- Handle Windows drive letter (e.g. "C:") at the beginning (if present)
                        if start == 3 then
                            filn = string.sub(t.value, 1, 3) .. filn
                        end

                        return filn, lnum, col, text
                    end
                else
                    return function(t)
                        local _, _, filn, lnum, col, text =
                            string.find(t, [[([^:]+):(%d+):(%d+):(.*)]])
                        return filn, lnum, col, text
                    end
                end
            end)()

            local parse = function(t)
                -- print("t: ", vim.inspect(t))
                local filn, lnum, col, text = find(t.value)

                local ok
                ok, lnum = pcall(tonumber, lnum)
                if not ok then
                    lnum = nil
                end

                ok, col = pcall(tonumber, col)
                if not ok then
                    col = nil
                end

                t.filn = filn
                t.lnum = lnum
                t.col = col
                t.text = text

                return { filn, lnum, col, text }
            end

            local function entry_maker(_)
                local mt_vimgrep_entry

                opts = opts or {}

                -- local disable_devicons = opts.disable_devicons
                -- local disable_coordinates = opts.disable_coordinates or true
                local only_sort_text = opts.only_sort_text

                local execute_keys = {
                    path = function(t)
                        if Path:new(t.filename):is_absolute() then
                            return t.filename, false
                        else
                            return Path:new({ t.cwd, t.filename }):absolute(),
                                false
                        end
                    end,
                    filename = function(t)
                        return parse(t)[1], true
                    end,
                    lnum = function(t)
                        return parse(t)[2], true
                    end,
                    col = function(t)
                        return parse(t)[3], true
                    end,
                    text = function(t)
                        return parse(t)[4], true
                    end,
                }

                -- For text search only, the ordinal value is actually the text.
                if only_sort_text then
                    execute_keys.ordinal = function(t)
                        return t.text
                    end
                end

                mt_vimgrep_entry = {
                    cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()),
                    __index = function(t, k)
                        local raw = rawget(mt_vimgrep_entry, k)
                        if raw then
                            return raw
                        end

                        local executor = rawget(execute_keys, k)
                        if executor then
                            local val, save = executor(t)
                            if save then
                                rawset(t, k, val)
                            end
                            return val
                        end

                        return rawget(t, rawget(lookup_keys, k))
                    end,
                }

                --
                if opts.show_link_counts then
                    mt_vimgrep_entry.display = make_display
                else
                    mt_vimgrep_entry.display = iconic_display
                end

                return function(line)
                    return setmetatable({ line }, mt_vimgrep_entry)
                end
            end

            opts.entry_maker = entry_maker(opts)

            local live_grepper = finders.new_job(
                function(prompt)
                    if not prompt or prompt == "" then
                        return nil
                    end

                    local search_command = {
                        "rg",
                        "--vimgrep",
                        "-e",
                        "^#+\\s" .. prompt,
                        "--",
                    }
                    if search_mode == "para" then
                        search_command = {
                            "rg",
                            "--vimgrep",
                            "-e",
                            "\\^" .. prompt,
                            "--",
                        }
                    end

                    if search_mode == "tag" then
                        search_command = {
                            "rg",
                            "--vimgrep",
                            globArg,
                            "-e",
                            prompt,
                            "--",
                        }
                    end

                    if #filename_part > 0 then
                        table.insert(search_command, filename_part)
                    else
                        table.insert(search_command, cwd)
                    end

                    local ret = vim.tbl_flatten({ search_command })
                    return ret
                end,
                opts.entry_maker or make_entry.gen_from_vimgrep(opts),
                opts.max_results,
                opts.cwd
            )

            -- builtin.live_grep({
            local picker = pickers.new(opts, {
                cwd = cwd,
                prompt_title = "Notes referencing `" .. title .. "`",
                default_text = search_pattern,
                initial_mode = "insert",
                -- link to specific file (a daily file): [[2021-02-22]]
                -- link to heading in specific file (a daily file): [[2021-02-22#Touchpoint]]
                -- link to heading globally [[#Touchpoint]]
                -- link to heading in specific file (a daily file): [[The cool note#^xAcSh-xxr]]
                -- link to paragraph globally [[#^xAcSh-xxr]]
                finder = live_grepper,
                previewer = conf.grep_previewer(opts),
                sorter = sorters.highlighter_only(opts),
                attach_mappings = function(_, map)
                    actions.select_default:replace(
                        picker_actions.select_default
                    )
                    map("i", "<c-y>", picker_actions.yank_link(opts))
                    map("i", "<c-i>", picker_actions.paste_link(opts))
                    map("n", "<c-y>", picker_actions.yank_link(opts))
                    map("n", "<c-i>", picker_actions.paste_link(opts))
                    map("i", "<c-cr>", picker_actions.paste_link(opts))
                    map("n", "<c-cr>", picker_actions.paste_link(opts))
                    return true
                end,
            })
            picker:find()
        end
    end)
end

--
-- GotoThisWeek:
-- -------------
--
-- find this week's weekly note and create it if necessary.
--
local function GotoThisWeek(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.journal_auto_open = opts.journal_auto_open or M.Cfg.journal_auto_open

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local dinfo =
            dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday)
        local title = dinfo.isoweek
        local fname = M.Cfg.weeklies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            if opts.journal_auto_open then
                if opts.calendar == true then
                    -- ensure that the calendar window is not improperly overwritten
                    vim.cmd("wincmd w")
                end
                vim.cmd("e " .. fname)
            else
                find_files_sorted({
                    prompt_title = "Goto this week:",
                    cwd = M.Cfg.weeklies,
                    default_text = title,
                    find_command = M.Cfg.find_command,
                    attach_mappings = function(_, map)
                        actions.select_default:replace(
                            picker_actions.select_default
                        )
                        map("i", "<c-y>", picker_actions.yank_link(opts))
                        map("i", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-y>", picker_actions.yank_link(opts))
                        map("n", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-c>", picker_actions.close(opts))
                        map("n", "<esc>", picker_actions.close(opts))
                        return true
                    end,
                })
            end
        end

        if
            (fexists ~= true)
            and (
                (opts.weeklies_create_nonexisting == true)
                or M.Cfg.weeklies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.weekly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end

        picker()
    end)
end

--
-- GotoThisMonth:
-- -------------
--
-- find this month's monthly note and create it if necessary.
--
local function GotoThisMonth(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.journal_auto_open = opts.journal_auto_open or M.Cfg.journal_auto_open

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local dinfo =
            dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday)
        local title = dinfo.month_ym
        local fname = M.Cfg.monthlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            if opts.journal_auto_open then
                if opts.calendar == true then
                    -- ensure that the calendar window is not improperly overwritten
                    vim.cmd("wincmd w")
                end
                vim.cmd("e " .. fname)
            else
                find_files_sorted({
                    prompt_title = "Goto this month:",
                    cwd = M.Cfg.monthlies,
                    default_text = title,
                    find_command = M.Cfg.find_command,
                    attach_mappings = function(_, map)
                        actions.select_default:replace(
                            picker_actions.select_default
                        )
                        map("i", "<c-y>", picker_actions.yank_link(opts))
                        map("i", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-y>", picker_actions.yank_link(opts))
                        map("n", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-c>", picker_actions.close(opts))
                        map("n", "<esc>", picker_actions.close(opts))
                        return true
                    end,
                })
            end
        end

        if
            (fexists ~= true)
            and (
                (opts.monthlies_create_nonexisting == true)
                or M.Cfg.monthlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.monthly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end

        picker()
    end)
end

--
-- GotoThisQuarter:
-- -------------
--
-- find this quarter's quarterly note and create it if necessary.
--
local function GotoThisQuarter(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.journal_auto_open = opts.journal_auto_open or M.Cfg.journal_auto_open

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        -- Use direct value instead of dateformat.quarter_yq, because os.date doesn't properly expand to a date
        local dinfo =
            dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday)
        local title = dinfo.quarter_yq
        local fname = M.Cfg.quarterlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            if opts.journal_auto_open then
                if opts.calendar == true then
                    -- ensure that the calendar window is not improperly overwritten
                    vim.cmd("wincmd w")
                end
                vim.cmd("e " .. fname)
            else
                find_files_sorted({
                    prompt_title = "Goto this quarter:",
                    cwd = M.Cfg.quarterlies,
                    default_text = title,
                    find_command = M.Cfg.find_command,
                    attach_mappings = function(_, map)
                        actions.select_default:replace(
                            picker_actions.select_default
                        )
                        map("i", "<c-y>", picker_actions.yank_link(opts))
                        map("i", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-y>", picker_actions.yank_link(opts))
                        map("n", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-c>", picker_actions.close(opts))
                        map("n", "<esc>", picker_actions.close(opts))
                        return true
                    end,
                })
            end
        end

        if
            (fexists ~= true)
            and (
                (opts.quarterlies_create_nonexisting == true)
                or M.Cfg.quarterlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.quarterly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end

        picker()
    end)
end

--
-- GotoThisYear:
-- -------------
--
-- find this year's yearly note and create it if necessary.
--
local function GotoThisYear(opts)
    opts = opts or {}
    opts.insert_after_inserting = opts.insert_after_inserting
        or M.Cfg.insert_after_inserting
    opts.close_after_yanking = opts.close_after_yanking
        or M.Cfg.close_after_yanking
    opts.journal_auto_open = opts.journal_auto_open or M.Cfg.journal_auto_open

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local dinfo =
            dateutils.calculate_dates(nil, M.Cfg.calendar_opts.calendar_monday)
        local title = dinfo.year
        local fname = M.Cfg.yearlies .. "/" .. title .. M.Cfg.extension
        local fexists = fileutils.file_exists(fname)
        local function picker()
            if opts.journal_auto_open then
                if opts.calendar == true then
                    -- ensure that the calendar window is not improperly overwritten
                    vim.cmd("wincmd w")
                end
                vim.cmd("e " .. fname)
            else
                find_files_sorted({
                    prompt_title = "Goto this year:",
                    cwd = M.Cfg.yearlies,
                    default_text = title,
                    find_command = M.Cfg.find_command,
                    attach_mappings = function(_, map)
                        actions.select_default:replace(
                            picker_actions.select_default
                        )
                        map("i", "<c-y>", picker_actions.yank_link(opts))
                        map("i", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-y>", picker_actions.yank_link(opts))
                        map("n", "<c-i>", picker_actions.paste_link(opts))
                        map("n", "<c-c>", picker_actions.close(opts))
                        map("n", "<esc>", picker_actions.close(opts))
                        return true
                    end,
                })
            end
        end

        if
            (fexists ~= true)
            and (
                (opts.yearlies_create_nonexisting == true)
                or M.Cfg.yearlies_create_nonexisting == true
            )
        then
            create_note_from_template(
                title,
                nil,
                fname,
                M.note_type_templates.yearly,
                nil,
                function()
                    opts.erase = true
                    opts.erase_file = fname
                    picker()
                end
            )
            return
        end

        picker()
    end)
end

--
-- Calendar Stuff
-- --------------

-- return if a daily 'note exists' indicator (sign) should be displayed for a particular day
local function CalendarSignDay(day, month, year)
    local fn = M.Cfg.dailies
        .. "/"
        .. string.format("%04d-%02d-%02d", year, month, day)
        .. M.Cfg.extension
    if fileutils.file_exists(fn) then
        return 1
    end
    return 0
end

-- action on enter on a specific day:
-- preview in telescope, stay in calendar on cancel, open note in other window on accept
local function CalendarAction(day, month, year, _, _)
    local opts = {}
    opts.date = string.format("%04d-%02d-%02d", year, month, day)
    opts.date_table = { year = year, month = month, day = day }
    opts.calendar = true
    GotoDate(opts)
end

local function ShowCalendar(opts)
    local defaults = {}
    defaults.cmd = "CalendarVR"
    defaults.vertical_resize = 1

    opts = opts or defaults
    vim.cmd(opts.cmd)
    if opts.vertical_resize then
        vim.cmd("vertical resize +" .. opts.vertical_resize)
    end
    vim.cmd([[
      set signcolumn=no
      set nonumber
      set norelativenumber
    ]])
end

-- set up calendar integration: forward to our lua functions
local function SetupCalendar(opts)
    local defaults = M.Cfg.calendar_opts
    opts = opts or defaults

    local cmd = [[
        function! MyCalSign(day, month, year)
            return luaeval('require("telekasten").CalendarSignDay(_A[1], _A[2], _A[3])', [a:day, a:month, a:year])
        endfunction

        function! MyCalAction(day, month, year, weekday, dir)
            " day : day
            " month : month
            " year year
            " weekday : day of week (monday=1)
            " dir : direction of calendar
            return luaeval('require("telekasten").CalendarAction(_A[1], _A[2], _A[3], _A[4], _A[5])',
                                                                 \ [a:day, a:month, a:year, a:weekday, a:dir])
        endfunction

        function! MyCalBegin()
            " too early, windown doesn't exist yet
            " cannot resize
        endfunction

        let g:calendar_sign = 'MyCalSign'
        let g:calendar_action = 'MyCalAction'
        " let g:calendar_begin = 'MyCalBegin'

        let g:calendar_mark = '{{calendar_mark}}'
        let g:calendar_weeknm = {{weeknm}}
    ]]

    for k, v in pairs(opts) do
        cmd = cmd:gsub("{{" .. k .. "}}", v)
    end
    vim.cmd(cmd)
    if opts.calendar_monday == 1 then
        vim.cmd("let g:calendar_monday = 1")
    end
end

local function ToggleTodo(opts)
    -- replace
    --       by -
    -- -     by - [ ]
    -- - [ ] by - [x]
    -- - [x] by -
    -- enter insert mode if opts.i == true
    -- if opts.v = true, then look for marks to toggle
    opts = opts or {}
    local startline = vim.api.nvim_buf_get_mark(0, "<")[1]
    local endline = vim.api.nvim_buf_get_mark(0, ">")[1]
    local cursorlinenr = vim.api.nvim_win_get_cursor(0)[1]
    -- to avoid the visual range marks not being reset when calling
    -- command from normal mode
    vim.api.nvim_buf_set_mark(0, "<", 0, 0, {})
    vim.api.nvim_buf_set_mark(0, ">", 0, 0, {})
    if startline <= 0 or endline <= 0 or opts.v ~= true then
        startline = cursorlinenr
        endline = cursorlinenr
    end
    for curlinenr = startline, endline do
        local curline =
            vim.api.nvim_buf_get_lines(0, curlinenr - 1, curlinenr, false)[1]
        local stripped = vim.trim(curline)
        local repline
        if
            vim.startswith(stripped, "- ")
            and not vim.startswith(stripped, "- [")
        then
            repline = curline:gsub("%- ", "- [ ] ", 1)
        else
            if vim.startswith(stripped, "- [ ]") then
                repline = curline:gsub("%- %[ %]", "- [x]", 1)
            else
                if vim.startswith(stripped, "- [x]") then
                    if opts.onlyTodo then
                        repline = curline:gsub("%- %[x%]", "- [ ]", 1)
                    else
                        repline = curline:gsub("%- %[x%]", "-", 1)
                    end
                else
                    repline = curline:gsub("(%S)", "- [ ] %1", 1)
                end
            end
        end
        vim.api.nvim_buf_set_lines(
            0,
            curlinenr - 1,
            curlinenr,
            false,
            { repline }
        )
        if opts.i then
            vim.api.nvim_feedkeys("A", "m", false)
        end
    end
end

local function FindAllTags(opts)
    opts = opts or {}
    local i = opts.i
    opts.cwd = M.Cfg.home
    opts.tag_notation = M.Cfg.tag_notation
    local templateDir = Path:new(M.Cfg.templates):make_relative(M.Cfg.home)
    opts.templateDir = templateDir
    opts.rg_pcre = M.Cfg.rg_pcre

    global_dir_check(function(dir_check)
        if not dir_check then
            return
        end

        local tag_map = tagutils.do_find_all_tags(opts)
        local taglist = {}

        local max_tag_len = 0
        for k, v in pairs(tag_map) do
            taglist[#taglist + 1] = { tag = k, details = v }
            if #k > max_tag_len then
                max_tag_len = #k
            end
        end

        if M.Cfg.show_tags_theme == "get_cursor" then
            opts = themes.get_cursor({
                layout_config = {
                    height = math.min(math.floor(vim.o.lines * 0.8), #taglist),
                },
            })
        elseif M.Cfg.show_tags_theme == "ivy" then
            opts = themes.get_ivy({
                layout_config = {
                    prompt_position = "top",
                    height = math.min(math.floor(vim.o.lines * 0.8), #taglist),
                },
            })
        else
            opts = themes.get_dropdown({
                layout_config = {
                    prompt_position = "top",
                    height = math.min(math.floor(vim.o.lines * 0.8), #taglist),
                },
            })
        end
        -- re-apply
        opts.cwd = M.Cfg.home
        opts.tag_notation = M.Cfg.tag_notation
        opts.i = i
        pickers
            .new(opts, {
                prompt_title = "Tags",
                finder = finders.new_table({
                    results = taglist,
                    entry_maker = function(entry)
                        return {
                            value = entry,
                            -- display = entry.tag .. ' \t (' .. #entry.details .. ' matches)',
                            display = string.format(
                                "%" .. max_tag_len .. "s ... (%3d matches)",
                                entry.tag,
                                #entry.details
                            ),
                            ordinal = entry.tag,
                        }
                    end,
                }),
                sorter = conf.generic_sorter(opts),
                attach_mappings = function(prompt_bufnr, map)
                    actions.select_default:replace(function()
                        -- actions for insert tag, default action: search for tag
                        local selection =
                            action_state.get_selected_entry().value.tag
                        local follow_opts = {
                            follow_tag = selection,
                            show_link_counts = false,
                            templateDir = templateDir,
                        }
                        actions._close(prompt_bufnr, false)
                        vim.schedule(function()
                            FollowLink(follow_opts)
                        end)
                    end)
                    map("i", "<c-y>", picker_actions.yank_tag(opts))
                    map("i", "<c-i>", picker_actions.paste_tag(opts))
                    map("n", "<c-y>", picker_actions.yank_tag(opts))
                    map("n", "<c-i>", picker_actions.paste_tag(opts))
                    map("n", "<c-c>", picker_actions.close(opts))
                    map("n", "<esc>", picker_actions.close(opts))
                    return true
                end,
            })
            :find()
    end)
end

-- Setup(cfg)
--
-- Overrides config with elements from cfg. See top of file for defaults.
--
local function Setup(cfg)
    cfg = cfg or {}
    defaultConfig(cfg.home)
    local debug = cfg.debug
    for k, v in pairs(cfg) do
        -- merge everything but calendar opts
        -- they will be merged later
        if k ~= "calendar_opts" then
            if k == "home" then
                v = v
            end
            M.Cfg[k] = v
            if debug then
                print(
                    "Setup() setting `"
                        .. k
                        .. "`   ->   `"
                        .. tostring(v)
                        .. "`"
                )
            end
        end
    end
    -- TODO: this is obsolete:
    if vim.fn.executable("rg") == 1 then
        M.Cfg.find_command = { "rg", "--files", "--sortr", "created" }
    else
        M.Cfg.find_command = nil
    end

    -- this looks a little messy
    if M.Cfg.plug_into_calendar then
        cfg.calendar_opts = cfg.calendar_opts or {}
        M.Cfg.calendar_opts = M.Cfg.calendar_opts or {}
        M.Cfg.calendar_opts.weeknm = cfg.calendar_opts.weeknm
            or M.Cfg.calendar_opts.weeknm
            or 1
        M.Cfg.calendar_opts.calendar_monday = cfg.calendar_opts.calendar_monday
            or M.Cfg.calendar_opts.calendar_monday
            or 1
        M.Cfg.calendar_opts.calendar_mark = cfg.calendar_opts.calendar_mark
            or M.Cfg.calendar_opts.calendar_mark
            or "left-fit"
        SetupCalendar(M.Cfg.calendar_opts)
    end

    -- setup extensions to filter for
    M.Cfg.filter_extensions = cfg.filter_extensions or { M.Cfg.extension }

    -- provide fake filenames for template loading to fail silently if template is configured off
    M.Cfg.template_new_note = M.Cfg.template_new_note or "none"
    M.Cfg.template_new_daily = M.Cfg.template_new_daily or "none"
    M.Cfg.template_new_weekly = M.Cfg.template_new_weekly or "none"
    M.Cfg.template_new_monthly = M.Cfg.template_new_monthly or "none"
    M.Cfg.template_new_quarterly = M.Cfg.template_new_quarterly or "none"
    M.Cfg.template_new_yearly = M.Cfg.template_new_yearly or "none"

    -- refresh templates
    M.note_type_templates = {
        normal = M.Cfg.template_new_note,
        daily = M.Cfg.template_new_daily,
        weekly = M.Cfg.template_new_weekly,
        monthly = M.Cfg.template_new_monthly,
        quarterly = M.Cfg.template_new_quarterly,
        yearly = M.Cfg.template_new_yearly,
    }

    -- for previewers to pick up our syntax, we need to tell plenary to override `.md` with our syntax
    if M.Cfg.auto_set_filetype or M.Cfg.auto_set_syntax then
        filetype.add_file("telekasten")
    end
    -- setting the syntax moved into plugin/telekasten.vim
    -- and does not work

    if M.Cfg.take_over_my_home == true then
        if M.Cfg.auto_set_filetype then
            vim.cmd(
                "au BufEnter "
                    .. M.Cfg.home
                    .. "/*"
                    .. M.Cfg.extension
                    .. " set ft=telekasten"
            )
        end
    end

    if debug then
        print("Resulting config:")
        print("-----------------")
        print(vim.inspect(M.Cfg))
    end

    -- Convert all directories in full path
    M.Cfg.image_subdir = make_config_path_absolute(M.Cfg.image_subdir)
    M.Cfg.dailies = make_config_path_absolute(M.Cfg.dailies)
    M.Cfg.weeklies = make_config_path_absolute(M.Cfg.weeklies)
    M.Cfg.monthlies = make_config_path_absolute(M.Cfg.monthlies)
    M.Cfg.quarterlies = make_config_path_absolute(M.Cfg.quarterlies)
    M.Cfg.yearlies = make_config_path_absolute(M.Cfg.yearlies)
    M.Cfg.templates = make_config_path_absolute(M.Cfg.templates)

    -- Check if ripgrep is compiled with --pcre
    -- ! This will need to be fixed when neovim moves to lua >=5.2 by the following:
    -- M.Cfg.rg_pcre = os.execute("echo 'hello' | rg --pcr2 hello &> /dev/null") or false

    M.Cfg.rg_pcre = false
    local has_pcre =
        os.execute("echo 'hello' | rg --pcre2 hello > /dev/null 2>&1")
    if has_pcre == 0 then
        M.Cfg.rg_pcre = true
    end
    M.Cfg.media_previewer = M.Cfg.media_previewer
    M.Cfg.media_extensions = M.Cfg.media_extensions
end

local function _setup(cfg)
    if cfg.vaults ~= nil and cfg.default_vault ~= nil then
        M.vaults = cfg.vaults
        cfg.vaults = nil
        Setup(M.vaults[cfg.default_vault])
    elseif cfg.vaults ~= nil and cfg.vaults["default"] ~= nil then
        M.vaults = cfg.vaults
        cfg.vaults = nil
        Setup(M.vaults["default"])
    elseif cfg.home ~= nil then
        M.vaults = cfg.vaults or {}
        cfg.vaults = nil
        M.vaults["default"] = cfg
        Setup(cfg)
    end
end

local function ChangeVault(opts)
    tkpickers.vaults(M, opts)
end

local function chdir(cfg)
    Setup(cfg)
    -- M.Cfg = vim.tbl_deep_extend("force", defaultConfig(new_home), cfg)
end

M.find_notes = FindNotes
M.find_daily_notes = FindDailyNotes
M.search_notes = SearchNotes
M.insert_link = InsertLink
M.follow_link = FollowLink
M.setup = _setup
M.goto_today = GotoToday
M.new_note = CreateNote
M.goto_thisweek = GotoThisWeek
M.find_weekly_notes = FindWeeklyNotes
M.goto_thismonth = GotoThisMonth
M.find_monthly_notes = FindMonthlyNotes
M.goto_thisquarter = GotoThisQuarter
M.find_quarterly_notes = FindQuarterlyNotes
M.goto_thisyear = GotoThisYear
M.find_yearly_notes = FindYearlyNotes
M.yank_notelink = YankLink
M.rename_note = RenameNote
M.new_templated_note = CreateNoteSelectTemplate
M.show_calendar = ShowCalendar
M.CalendarSignDay = CalendarSignDay
M.CalendarAction = CalendarAction
M.paste_img_and_link = imgFromClipboard
M.toggle_todo = ToggleTodo
M.show_backlinks = ShowBacklinks
M.find_friends = FindFriends
M.insert_img_link = InsertImgLink
M.preview_img = PreviewImg
M.browse_media = BrowseImg
M.taglinks = taglinks
M.show_tags = FindAllTags
M.switch_vault = ChangeVault
M.chdir = chdir

-- Telekasten command, completion
local TelekastenCmd = {
    commands = function()
        return {
            { "find notes", "find_notes", M.find_notes },
            { "find daily notes", "find_daily_notes", M.find_daily_notes },
            { "search in notes", "search_notes", M.search_notes },
            { "insert link", "insert_link", M.insert_link },
            { "follow link", "follow_link", M.follow_link },
            { "goto today", "goto_today", M.goto_today },
            { "new note", "new_note", M.new_note },
            { "goto thisweek", "goto_thisweek", M.goto_thisweek },
            { "find weekly notes", "find_weekly_notes", M.find_weekly_notes },
            { "goto thismonth", "goto_thismonth", M.goto_thismonth },
            {
                "find monthly notes",
                "find_monthly_notes",
                M.find_monthly_notes,
            },
            { "goto thisquarter", "goto_thisquarter", M.goto_thisquarter },
            {
                "find quarterly notes",
                "find_quarterly_notes",
                M.find_quarterly_notes,
            },
            { "goto thisyear", "goto_thisyear", M.goto_thisyear },
            {
                "find yearly notes",
                "find_yearly_notes",
                M.find_yearly_notes,
            },
            { "yank link to note", "yank_notelink", M.yank_notelink },
            { "rename note", "rename_note", M.rename_note },
            {
                "new templated note",
                "new_templated_note",
                M.new_templated_note,
            },
            { "show calendar", "show_calendar", M.show_calendar },
            {
                "paste image from clipboard",
                "paste_img_and_link",
                M.paste_img_and_link,
            },
            { "toggle todo", "toggle_todo", M.toggle_todo },
            { "show backlinks", "show_backlinks", M.show_backlinks },
            { "find friend notes", "find_friends", M.find_friends },
            {
                "browse images, insert link",
                "insert_img_link",
                M.insert_img_link,
            },
            { "preview image under cursor", "preview_img", M.preview_img },
            { "browse media", "browse_media", M.browse_media },
            { "panel", "panel", M.panel },
            { "show tags", "show_tags", M.show_tags },
            { "switch vault", "switch_vault", M.switch_vault },
        }
    end,
}

TelekastenCmd.command = function(subcommand)
    local show = function(opts)
        opts = opts or {}
        pickers
            .new(opts, {
                prompt_title = "Command palette",
                finder = finders.new_table({
                    results = TelekastenCmd.commands(),
                    entry_maker = function(entry)
                        return {
                            value = entry,
                            display = entry[1],
                            ordinal = entry[2],
                        }
                    end,
                }),
                sorter = conf.generic_sorter(opts),
                attach_mappings = function(prompt_bufnr, _)
                    actions.select_default:replace(function()
                        -- important: actions.close(bufnr) is not enough
                        -- it resulted in: preview_img NOT receiving the prompt as default text
                        -- apparently it has sth to do with keeping insert mode
                        actions._close(prompt_bufnr, true)

                        local selection =
                            action_state.get_selected_entry().value[3]
                        selection()
                    end)
                    return true
                end,
            })
            :find()
    end
    if subcommand then
        -- print("trying subcommand " .. "`" .. subcommand .. "`")
        for _, entry in pairs(TelekastenCmd.commands()) do
            if entry[2] == subcommand then
                local selection = entry[3]
                selection()
                return
            end
        end
        print("No such subcommand: `" .. subcommand .. "`")
    else
        local theme

        if M.Cfg.command_palette_theme == "ivy" then
            theme = themes.get_ivy()
        else
            theme = themes.get_dropdown({
                layout_config = { prompt_position = "top" },
            })
        end
        show(theme)
    end
end
function picker_actions.create_new(opts)
    opts = opts or {}
    opts.subdirs_in_links = opts.subdirs_in_links or M.Cfg.subdirs_in_links
    return function(prompt_bufnr)
        local prompt =
            action_state.get_current_picker(prompt_bufnr).sorter._discard_state.prompt
        actions.close(prompt_bufnr)
        on_create(opts, prompt)
        -- local selection = action_state.get_selected_entry()
    end
end

-- nvim completion function for completing :Telekasten sub-commands
TelekastenCmd.complete = function()
    local candidates = {}
    for k, v in pairs(TelekastenCmd.commands()) do
        candidates[k] = v[2]
    end
    return candidates
end

M.panel = TelekastenCmd.command
M.Command = TelekastenCmd

return M
